Repository navigation
[RF] Skip clean nodes in the CUDA evaluation path of RooFit::Evaluator - #23753
Merged
guitargeek merged 1 commit intoOct 19, 2026
Merged
guitargeek merged 1 commit into
guitargeek merged 1 commit into
Conversation
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
Test Results 23 files 23 suites 3d 22h 43m 23s ⏱️ For more details on these failures, see this check. Results for commit a14b097. |
dpiparo
approved these changes
Oct 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):