Skip to content

[RF] Skip clean nodes in the CUDA evaluation path of RooFit::Evaluator - #23753

Merged
guitargeek merged 1 commit into
root-project:masterfrom
guitargeek:roofit-cuda-evaluator-dirty-tracking
Oct 19, 2026
Merged

guitargeek merged 1 commit into
root-project:masterfrom
guitargeek:roofit-cuda-evaluator-dirty-tracking

Conversation

@guitargeek

@guitargeek guitargeek commented Oct 18, 2026 •

Copy link
Copy Markdown
Contributor

The CPU path in RooFit::Evaluator::run() only recomputes nodes that are dirty, i.e. that depend on a variable whose value changed since the last evaluation. The CUDA path in getValHeterogeneous() re-enqueued the whole computation graph on every call, because the buffers of intermediate results were released as soon as the clients of a node were computed, so nothing could be reused.

This commit uses the same dirty tracking in the CUDA path: the output buffers of the nodes now persist across evaluations, and a node is only recomputed if one of its value servers was recomputed in the same evaluation or if a variable it depends on changed. Since no buffer is recycled within an evaluation anymore, the special handling of pinned buffers for the eager release is not needed either.

During a minimization, most likelihood evaluations change only one or two parameters: the numeric gradient varies one parameter at a time, and Hesse two. For a RooAddPdf with several components, only the component that depends on the varied parameter needs to be evaluated again, and for a varied yield no component kernel has to run at all. Measured with the FitModel case from rootbench (5-component extended RooAddPdf, 15 parameters, Minuit2) on an RTX A4500, with identical results, number of function calls and parameter errors before and after:

events before after
10k 0.29 s 0.12 s
100k 0.77 s 0.31 s
1M 5.05 s 2.12 s

At 1M events the kernel time per likelihood evaluation goes from 2.2 ms to 0.94 ms, with 5.4 instead of 13 kernel launches and 5.6 instead of 17 memcpys per evaluation on average.

The price is that the device memory for the intermediate results of all vector nodes stays allocated for the lifetime of the Evaluator instead of being recycled between nodes of the same size. For the usual computation graphs this is a few arrays of the size of the dataset.

🤖 Done with the help of AI

The unbinned RooFit benchmark with this PR looks as follows (about 2x faster Cuda fits than before this PR, clearly separating the CUDA backend again from RooFit IMT):

benchRooFitBackends

The CPU path in RooFit::Evaluator::run() only recomputes nodes that are
dirty, i.e. that depend on a variable whose value changed since the last
evaluation. The CUDA path in getValHeterogeneous() re-enqueued the whole
computation graph on every call, because the buffers of intermediate
results were released as soon as the clients of a node were computed, so
nothing could be reused.

This commit uses the same dirty tracking in the CUDA path: the output
buffers of the nodes now persist across evaluations, and a node is only
recomputed if one of its value servers was recomputed in the same
evaluation or if a variable it depends on changed. Since no buffer is
recycled within an evaluation anymore, the special handling of pinned
buffers for the eager release is not needed either.

During a minimization, most likelihood evaluations change only one or
two parameters: the numeric gradient varies one parameter at a time, and
Hesse two. For a RooAddPdf with several components, only the component
that depends on the varied parameter needs to be evaluated again, and
for a varied yield no component kernel has to run at all. Measured with
the FitModel case from rootbench (5-component extended RooAddPdf, 15
parameters, Minuit2) on an RTX A4500, with identical results, number of
function calls and parameter errors before and after:

  events    before     after
  10k       0.29 s     0.12 s
  100k      0.77 s     0.31 s
  1M        5.05 s     2.12 s

At 1M events the kernel time per likelihood evaluation goes from 2.2 ms
to 0.94 ms, with 5.4 instead of 13 kernel launches and 5.6 instead of 17
memcpys per evaluation on average.

The price is that the device memory for the intermediate results of all
vector nodes stays allocated for the lifetime of the Evaluator instead
of being recycled between nodes of the same size. For the usual
computation graphs this is a few arrays of the size of the dataset.

🤖 Done with the help of AI
@github-actions

Copy link
Copy Markdown

Test Results

    23 files      23 suites   3d 22h 43m 23s ⏱️
 3 889 tests  3 888 ✅ 0 💤 1 ❌
79 439 runs  79 438 ✅ 0 💤 1 ❌

For more details on these failures, see this check.

Results for commit a14b097.

@guitargeek
guitargeek merged commit e0e5723 into root-project:master Oct 19, 2026
41 of 44 checks passed
@guitargeek
guitargeek deleted the roofit-cuda-evaluator-dirty-tracking branch October 19, 2026 09:12
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