Skip to content

[RF] Fix quadratic scaling when building large workspaces - #23139

Merged
guitargeek merged 3 commits into
root-project:masterfrom
guitargeek:mmaf-scaling-18683
Aug 25, 2026
Merged

guitargeek merged 3 commits into
root-project:masterfrom
guitargeek:mmaf-scaling-18683

Conversation

@guitargeek

Copy link
Copy Markdown
Contributor

This PR addresses the pathological scaling of HistFactory's MakeModelAndMeasurementsFast() with model size reported in #18683. Rather than parallelizing over channels as the issue suggests, it removes the underlying quadratic bottlenecks, which profiling showed sit in RooFit core and are not HistFactory-specific:

  1. RooAbsCollection::sortTopologically() repeatedly moved single elements into place with linear searches and vector insertions. It is now an iterative depth-first post-order traversal that visits every arg and server edge once, reproducing the previous ordering -- including the same-name server instance substitution that graphs with cloned sub-trees (e.g. from createProjection()) rely on. Collections with duplicate names fall back to the previous algorithm.
  2. The RooRealIntegral constructor used linear RooAbsCollection::index() lookups and a recursive client-marking that revisited args once per path through the graph. Hash-map lookups and an iterative traversal make it linear in the graph size; the by-instance vs. by-name matching semantics are unchanged.
  3. RooWorkspace::import() with RecycleConflictNodes() deep-cloned the whole incoming graph only to discard the clones of nodes already in the workspace. It now clones only the new nodes and connects them directly to the existing ones. One deliberate behavior change: importWorkspaceHook() and the class-code import no longer run for recycled nodes, since there is no clone to run them on.

Benchmark from the issue (per channel: 100 samples, 200 systematics, 50 bins), runtime of MakeModelAndMeasurementsFast():

Setup before after
1 channel 38 s ~6 s
5 channels 128 s 30 s

Verified with the full RooFit test suite, all stressRooFit variants, and stressHistFactory; workspaces built with and without the import pruning were checked to be identical. A new test, RooWorkspace.ImportRecycleConflictNodes, covers the pruned import path, including recycled top-level nodes and re-imports of existing graphs.

A known remaining hotspot, the linear RooSTLRefCountList::Add/Remove (~23 % of the improved runtime), is left for a follow-up since fixing it touches server-order semantics.

Closes #18683

🤖 Done with the help of AI

The previous implementation repeatedly moved single elements into
place, each time with a linear search plus a linear-time vector
insertion, which is quadratic in the collection size. For the large
computation graphs of binned likelihood models, e.g. built by
HistFactory, this sort dominated the construction time of every
RooRealIntegral (it is also used in the RooFit::Evaluator setup).

Replace it with an iterative depth-first post-order traversal over the
server links that visits every arg and server edge only once. The
traversal reproduces the previous ordering, including the substitution
of same-name server instances, which matters for graphs containing
cloned sub-trees (e.g. from RooAbsReal::createProjection()).
Collections with multiple same-name entries fall back to the original
algorithm, which preserves their size. The new implementation also
terminates on cyclic graphs instead of looping forever.

🤖 Done with the help of AI
The server analysis in getValueAndShapeServers() looked up graph nodes
with the linear-scan RooAbsCollection::index(), and the recursive
marking of dependent value clients revisited args once per path through
the graph. Together with the previously quadratic topological sort,
this made constructing a RooRealIntegral scale quadratically with the
graph size, and the Asimov dataset generation the dominant cost of
MakeModelAndMeasurementsFast() for HistFactory models.

Build hash maps from the graph nodes to their index once and use them
for all lookups, and turn the marking recursion into an iterative
traversal that visits every arg at most once. Like before, dependent
clients are matched by instance and servers by name, which matters for
graphs containing cloned same-name sub-trees (e.g. from
RooAbsReal::createProjection()).

🤖 Done with the help of AI
When importing with RooFit::RecycleConflictNodes() and no renaming,
import() deep-cloned the entire incoming computation graph, only to
discard the clones of all nodes that already exist in the workspace.
For workspaces that are built up incrementally, like in HistFactory,
almost the whole graph is already present during later imports, so this
was wasted work that scaled quadratically with the model size.

In this mode, traverse the graph like the snapshot would, but clone
only the nodes whose names are not in the workspace yet and connect
them directly to the existing workspace nodes. Like before, new nodes
below recycled ones are still imported, the workspace copies win for
conflicting names, and the expensive-object cache payloads of recycled
names are imported from the global cache instance. One deliberate
difference: importWorkspaceHook() and the class-code import no longer
run for recycled nodes, since there is no clone to run them on; their
only lasting effect could have been auxiliary objects imported for a
clone that was thrown away right after.

For the HistFactory benchmark from issue root-project#18683 (one channel with 100
samples, 200 systematics, and 50 bins), the three commits together
reduce the runtime of MakeModelAndMeasurementsFast() from 38 s to about
6 s, and from 128 s to 30 s for five such channels.

🤖 Done with the help of AI
@github-actions

Copy link
Copy Markdown

Test Results

    23 files      23 suites   3d 11h 55m 17s ⏱️
 3 855 tests  3 853 ✅ 0 💤 2 ❌
78 574 runs  78 571 ✅ 0 💤 3 ❌

For more details on these failures, see this check.

Results for commit 881516a.

@dpiparo dpiparo modified the milestone: 6.40.04 Aug 24, 2026
@dpiparo
dpiparo self-requested a review August 25, 2026 08:33

@dpiparo dpiparo 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.

Thanks!

@guitargeek
guitargeek merged commit fba704b into root-project:master Aug 25, 2026
32 of 38 checks passed
@guitargeek
guitargeek deleted the mmaf-scaling-18683 branch August 25, 2026 08:38
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.

[RF] Parallelize per-channel workspace creation in MakeModelAndMeasurementsFast

2 participants