Skip to content

Produce multiple SimCluster collections in CaloTruthAccumulator#50578

Open
tcuisset wants to merge 3 commits intocms-sw:masterfrom
tcuisset:caloTruth-pr1
Open

Produce multiple SimCluster collections in CaloTruthAccumulator#50578
tcuisset wants to merge 3 commits intocms-sw:masterfrom
tcuisset:caloTruth-pr1

Conversation

@tcuisset
Copy link
Copy Markdown
Contributor

PR description:

CaloTruthAccumulator is updated to produce multiple SimCluster collections, to simplify the validation code and allow easier interpretation of simulation truth. The collections produced are:

  • CaloParticle collection (same as before)
  • SimCluster "CaloParticle" collection (same thing but in SimCluster dataformat, simplifies consumers to avoid a lof of if/else)
  • SimCluster "legacy" collection (identical behaviour as SimCluster before this commit, with the same empty label as before. Not really recommended to be used, but kept for compatibility)
  • SimCluster "boundary" collection : creates a SimCluster for every SimTrack crossing calorimeter boundary (and having cumulative simhits from itself or its simtrack descendents). Can be used in replacement of "legacy" collection (the only difference is for primary photons that do not convert before boundary).
  • SimCluster "merged" collection : merges very collimated SimTrack into one SimCluster (only from same CaloParticle). For now it uses fastjet anti-kt with very small distance parameter (0.05), at least until a better merging algorithm is implemented.

It also produces RefVector to map SimCluster to CaloParticle.

This PR only contains the change in CaloTruthAccumulator. Another PR will follow with the TICL validation changes to use the new SimClusters.

Documentation
Slides at TICL meeting
Slide 7 of HGCAL presentation at NGT MC truth workshop

PR validation:

Ran usual PR tests, and tested with an updated TICL validation (to be submitted to a future PR)
No changes in physics are expected from this PR, since the new collections are not used (for now).

@cmsbuild
Copy link
Copy Markdown
Contributor

cmsbuild commented Mar 27, 2026

cms-bot internal usage

@cmsbuild
Copy link
Copy Markdown
Contributor

+code-checks

Logs: https://cmssdt.cern.ch/SDT/code-checks/cms-sw-PR-50578/48754

@cmsbuild
Copy link
Copy Markdown
Contributor

A new Pull Request was created by @tcuisset for master.

It involves the following packages:

  • SimCalorimetry/Configuration (simulation)
  • SimDataFormats/CaloAnalysis (simulation)
  • SimGeneral/CaloAnalysis (simulation)
  • SimGeneral/MixingModule (simulation)

@civanch, @cmsbuild, @kpedro88, @mdhildreth can you please review it and eventually sign? Thanks.
@ReyerBand, @apsallid, @bsunanda, @denizsun, @fabiocos, @felicepantaleo, @makortel, @martinamalberti, @missirol, @mmusich, @rovere, @salimcerci, @sameasy, @slomeo, @thomreis, @wang0jin this is something you requested to watch as well.
@ftenchini, @mandrenguyen, @sextonkennedy you are the release manager for this.

cms-bot commands are listed here

Comment on lines +186 to +189
void clearHitsAndFractions() {
hits_.clear();
fractions_.clear();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

In SimDataFormats/CaloAnalysis/interface/SimCluster.h [source] we're using the "Clear-and-minimize" idiom.

Copy link
Copy Markdown
Contributor Author

@tcuisset tcuisset Mar 27, 2026

Choose a reason for hiding this comment

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

I did not know of this idiom, thanks ! Though it seems weird to me to de-allocate the entire vector before processing the next event, it would need to reallocate every time. Maybe calling hits_.shrink_to_fit(); after filling would be more efficient, but I don't know if there is a real memory issue here (these are only vectors of int/float)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This pattern is recommended when there is the potential for memory hoarding, which it seems like there could be in this case. There is indeed a tradeoff between reallocating and hoarding: reallocating makes the job slower, but hoarding can cause syskill if the job exhausts available memory.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Concerning shrink_to_fit(), note that it is non-binding, so it might as well do nothing.

Regarding the reallocation point, I'm also curious to know what is the most efficient way to do this in CMSSW.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It seems the clearHitsAndFractions function is only ever called once, by CaloTruthAccumulator, and for that use we are essentially clearing and refilling with the same length, so shrinking is not good idea.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Are CaloParticles really held from one event to the next?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I got confused between the vector<CaloParticle> and the vector<int> hits_ that is inside the CaloParticle. The former is kept per stream (just cleared), but the CaloParticle are not kept

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Regarding the reallocation point, I'm also curious to know what is the most efficient way to do this in CMSSW.

Within a module processing one Event, avoiding reallocations is usually a good idea (especially within loops).

Between Events, it is better to release the event, because memory hoarders are a big problem (as Kevin already wrote). One allocation per module per event is less than a rounding error when we have components doing tens of millions of memory allocations per Event.

Comment on lines +253 to +255
template <typename R>
requires std::ranges::input_range<R> && std::same_as<std::ranges::range_value_t<R>, SimCluster>
SimCluster SimCluster::mergeHitsFromCollection(R const &inputs) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Just a curiosity: why do you need the template at all, given that you require the SimCluster type for the elements of the iterable? Is this going to be used for different containers in your next PR? Aren't they all going to be of the SimClusterCollection type?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Indeed probably the template is not needed, but I had in mind that we may want to merge CaloParticle dataformat as well. With hindsight it was probably not needed, but since I already coded it and it works....


ret.hits_.reserve(acc_fractions.size());
ret.fractions_.reserve(acc_fractions.size());
for (auto hit_and_fraction : acc_fractions) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

const auto&

class SubClusterMergerBase {
public:
/// Called when a SimCluster is started (along with the index of the SimCluster in its output collection)
void start_subcluster(DecayChain::edge_descriptor e, const DecayChain &g, std::size_t currentSubClusterIndex);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what is the type/content of edge_descriptor? is it a heavy object that should be passed by reference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

It is a pair of unsigned long (indices of source and child vertex of the edge), so it can be passed by value

Comment on lines +105 to +106
ClusterParentIndexRecorderT caloParticleParentIndexRecorder,
ClusterParentIndexRecorderT subClusterToMergedClusterParentIndexRecorder,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

should these be passed by reference?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

These are always built with temporaries, so they should automatically get moved.
From here a temporary ClusterParentIndexRecorder is made :

std::make_tuple(simClusterMergerFastJet_config_.getMerger(
                  ClusterParentIndexRecorder(simClusterMergerFastJet_config_.clustersToCaloParticleMap,
                                             caloParticles_refProd_),
                  ClusterParentIndexRecorder(simClusterMergerFastJet_config_.subClustersToMergedClusterMap,
                                             boundarySimCluster_refProd_))))));

Comment on lines +576 to +578
legacySimClusters_config_(producesCollector, "MergedCaloTruth"),
boundarySimClusters_config_(producesCollector, "MergedCaloTruthBoundaryTrackSimCluster"),
caloParticleSimClusters_config_(producesCollector, "MergedCaloTruthCaloParticle"),
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

for my understanding, how much duplication in content is there between these three products?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

there is some duplication, since mostly the same hits are stored.
"legacy" and "boundary" simClusters are nearly identical (except some corner cases involving photons). Ultimately I think we should get rid completely of "legacy" SimCluster, but there is still code depending on it for now (though I plan for TICL to remove all dependency on "legacy").
The "CaloParticle-SimCluster" is indeed a duplicate of "CaloParticle". This is again a transition, I think it would be more convenient to use one dataformat for everything since SimCluster & CaloParticle are so similar. Ultimately we could migrate everything and get rid of "CaloParticle" as a dataformat, but it is a large change for a single PR

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Indeed, for "breaking" changes, we have the opportunity to integrate them in the upcoming CMSSW_20 cycle (or later) without needing to retain backward compatibility for Run ≤3. So as preparatory work for that kind of migration, this is acceptable.

Still, I wonder: is it possible to have switches or workflows that disable some of these collections, if they are not all needed for all use cases?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I could implement a switch if needed. Which options would be needed ? I was mostly thinking that we may want to turn off CaloTruthAccumulator completely when we don't care about calorimeter validation, rather than really wanting to tweak which collections get produced

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

From the description, it seems like legacy/new/both would be a valid set of choices.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I've implemented two switches: produceLegacySimCluster and produceBoundaryAndMergedSimCluster

@cmsbuild
Copy link
Copy Markdown
Contributor

+code-checks

Logs: https://cmssdt.cern.ch/SDT/code-checks/cms-sw-PR-50578/48756

@cmsbuild
Copy link
Copy Markdown
Contributor

Pull request #50578 was updated. @civanch, @cmsbuild, @kpedro88, @mdhildreth can you please check and sign again.

CaloTruthAccumulator is updated to produce multiple SimCluster collections:
 - CaloParticle collection
 - SimCluster "CaloParticle" collection (same thing but in SimCluster dataformat, simplifies consumers)
 - SimCluster "legacy" collection (identical behaviour as SimCluster before this commit)
 - SimCluster "boundary" collection : creates a SimCluster for every SimTrack crossing calorimeter boundary (and having cumulative simhits from itself or its simtrack descendents). Can be used in replacement of "legacy" collection (the only difference is for primary photons that do not convert before boundary).
 - SimCluster "merged" collection : merges very collimated SimTrack into one SimCluster (only from same CaloParticle). For now it uses fastjet anti-kt with very small distance parameter (0.05)
It also produces RefVector to map SimCluster to CaloParticle
@cmsbuild
Copy link
Copy Markdown
Contributor

+code-checks

Logs: https://cmssdt.cern.ch/SDT/code-checks/cms-sw-PR-50578/48757

@cmsbuild
Copy link
Copy Markdown
Contributor

Pull request #50578 was updated. @civanch, @cmsbuild, @kpedro88, @mdhildreth can you please check and sign again.

Comment on lines +76 to +79
void clear() {
outputClusters.clear();
clustersToCaloParticleMap.clear();
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

IIUC this clear() gets called in initializeEvent(), which hints that the contents would be kept alive from one Event to the next. If this is the case, it would be much better to release the memory at the end of the Event (i.e. in finalizeEvent(), and really release, not just clear()).

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Ah, on further look I see they are moved in

applyToSimClusterConfig([&event](auto &config) {
event.emplace(config.outputClusters_token, std::move(config.outputClusters));
event.emplace(config.clustersToCaloParticleMap_token, std::move(config.clustersToCaloParticleMap));
});

So the memory hoarding doesn't occur, never mind this comment.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In finalizeEvents the vectors outputClusters and clustersToCaloParticleMap are moved into the event with event.emplace, which I believe would leave them without any memory of their own (though it does not seem to be a C++ standards requirements). The clear is actually more to get the vector in a determined state (and not in "moved-from" undetermined state), it should already be at zero capacity at this point.

But probably adding a shrink_to_fit would not hurt

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

(ah wrote my comment at the same time as yours)

Comment on lines +741 to +746
applyToSimClusterConfig([&event](auto &config) {
event.emplace(config.outputClusters_token, std::move(config.outputClusters));
event.emplace(config.clustersToCaloParticleMap_token, std::move(config.clustersToCaloParticleMap));
});
event.emplace(simClusterMergerFastJet_config_.subClustersToMergedClusterMap_token,
std::move(simClusterMergerFastJet_config_.subClustersToMergedClusterMap));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

I think it would be clearer for a future reader if the default-constructed state of all of these moved-from objects would be restored here after the emplace() calls instead of in initializeEvent() (to avoid raising alarms for possible memory hoarding).

I guess the swap() pattern would give best guarantees the memory to be actually released.

Comment on lines +400 to +748
std::unordered_map<Index_t, float>().swap(m_detIdToTotalSimEnergy);
std::unordered_multimap<Barcode_t, Index_t>().swap(m_simHitBarcodeToIndex);
m_detIdToTotalSimEnergy.clear();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

The clear() does not release memory, I'd suggest to keep the existing swap() pattern.

…new" (boundary, merged, CaloParticle-SimCluster) SimCluster collections
@cmsbuild
Copy link
Copy Markdown
Contributor

+code-checks

Logs: https://cmssdt.cern.ch/SDT/code-checks/cms-sw-PR-50578/48761

@cmsbuild
Copy link
Copy Markdown
Contributor

Pull request #50578 was updated. @civanch, @cmsbuild, @kpedro88, @mdhildreth can you please check and sign again.

@mandrenguyen
Copy link
Copy Markdown
Contributor

please test

@cmsbuild
Copy link
Copy Markdown
Contributor

+1

Size: This PR adds an extra 16KB to repository
Summary: https://cmssdt.cern.ch/SDT/jenkins-artifacts/pull-request-integration/PR-91794b/52356/summary.html
COMMIT: f669da1
CMSSW: CMSSW_16_1_X_2026-03-30-2300/el8_amd64_gcc13
User test area: For local testing, you can use /cvmfs/cms-ci.cern.ch/week1/cms-sw/cmssw/50578/52356/install.sh to create a dev area with all the needed externals and cmssw changes.

  • Static analyzer reported errors, please check

Comparison Summary

Summary:

  • You potentially added 12 lines to the logs
  • ROOTFileChecks: Some differences in event products or their sizes found
  • Reco comparison results: 4 differences found in the comparisons
  • DQMHistoTests: Total files compared: 53
  • DQMHistoTests: Total histograms compared: 4186910
  • DQMHistoTests: Total failures: 3428
  • DQMHistoTests: Total nulls: 0
  • DQMHistoTests: Total successes: 4183462
  • DQMHistoTests: Total skipped: 20
  • DQMHistoTests: Total Missing objects: 0
  • DQMHistoSizes: Histogram memory added: 0.0 KiB( 52 files compared)
  • Checked 227 log files, 198 edm output root files, 53 DQM output files
  • TriggerResults: no differences found

Max Memory Comparisons exceeding threshold

@cms-sw/core-l2 , I found 1 workflow step(s) with memory usage exceeding the error threshold:

Expand to see workflows ...
  • Error: Workflow 34634.999_TTbar_14TeV+Run4D121PU_PMXS1S2PR step2 max memory diff 168.0 exceeds +/- 90.0 MiB

config.clearAndReleaseMemory();
});
if (produceBoundaryAndMergedSimCluster_)
event.emplace(simClusterMergerFastJet_config_.subClustersToMergedClusterMap_token,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

have you tested all the cases of enabling and disabling these produce switches? the produces<>() command is always called, but the collection is only added to the event conditionally if produceBoundaryAndMergedSimCluster_ is true

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I have tested it, the framework seems fine with calling "produces" but not actually putting anything in the event (I think this is how HLT works also)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

If it is known at a module construction time that some data product won't be produced at all in a job, it would be better to avoid the produces() call as well.

@kpedro88
Copy link
Copy Markdown
Contributor

are the comparison changes in HGCalValidator expected and understood?

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.

6 participants