Skip to content

Commit 9bea3f3

Browse files
sawenzelclaude
andcommitted
Add a multithreaded Geant4 mode to o2-sim
This adds the --isMT mode, in which one sim process runs Geant4 with one transport thread per sim worker. - Each Geant4 worker thread asks the primary server for its sub-events and sends its hits to the merger, as a worker process does. - The VMC application is cloned per thread, with its own stack, random generator, external decayer and data channel. - The threads share one request channel to the primary server. - Their data channels are closed before the process exits, so that the merger receives all queued hits. - A worker leaves its event loop once the server has no more work, after an empty event. - Detector clones get their own hit buffers, and the worker volume map points at the worker's own modules. - The media energy cuts and the worker-only Geant4 commands are applied on the worker threads. - The cached magnetic field values are invalidated at the start of each track. - ShmManager takes a mutex for its allocations, and the single sim process occupies the whole shared region. - 16 detectors are enabled for MT; EMCAL, HMPID, ZDC, FOCAL, FT0 and FV0 needed fixes to their copy constructor or destructor. - --isMT stops with an error for other engines, for Geant4 built without multithreading, for VecGeom navigation, and for detectors not yet enabled for it. https://its.cern.ch/jira/browse/O2-7305 vmc-project/geant4_vmc#92 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 56ab9f6 commit 9bea3f3

45 files changed

Lines changed: 810 additions & 62 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎Common/Utils/include/CommonUtils/ShmManager.h‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
#include <list>
2323
#include <cstddef>
2424
#include <atomic>
25+
#include <mutex>
2526

2627
#if !defined(__CINT__) && !defined(__MAKECINT__) && !defined(__ROOTCLING__) && !defined(__CLING__)
2728
#include <boost/interprocess/managed_external_buffer.hpp>
@@ -70,7 +71,8 @@ class ShmManager
7071

7172
// create the local segment
7273
// this will occupy a subregion of an already created global shared mem segment
73-
void occupySegment();
74+
// (all of it for the single process of multithreaded Geant4, whose threads share it)
75+
void occupySegment(bool wholeRegion = false);
7476

7577
// simply attaches to the global segment
7678
bool attachToGlobalSegment();
@@ -88,7 +90,7 @@ class ShmManager
8890
// returns if pointer is part of the shm region under control of this manager
8991
bool isPointerOk(void* ptr) const
9092
{
91-
return mBufferPtr && getPointerOffset(ptr) < SHMPOOLSIZE;
93+
return mBufferPtr && getPointerOffset(ptr) < mPoolSize;
9294
}
9395

9496
// returns if shared mem setup is correctly setup/operational
@@ -114,6 +116,8 @@ class ShmManager
114116
ShmMetaInfo* mSegInfoPtr = nullptr; // pointing to the meta information object
115117
bool mIsMaster = false; // true if the manager who allocated the region
116118
bool mIsOperational = false;
119+
size_t mPoolSize = SHMPOOLSIZE; // size of the region occupied by this process
120+
std::mutex mAllocMutex; // the boost buffer has no locking of its own; threads of multithreaded Geant4 share it
117121
// helper function
118122
void* tryAttach(bool& success);
119123
size_t getPointerOffset(void* ptr) const { return (size_t)((char*)ptr - (char*)mBufferPtr); }

‎Common/Utils/src/ShmManager.cxx‎

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ bool ShmManager::attachToGlobalSegment()
178178
return b;
179179
}
180180

181-
void ShmManager::occupySegment()
181+
void ShmManager::occupySegment(bool wholeRegion)
182182
{
183183
LOG(info) << "OCCUPYING A SEGMENT IN A SHARED REGION";
184184
// get information about the global shared mem in which to occupy a region
@@ -197,15 +197,17 @@ void ShmManager::occupySegment()
197197
if (b) {
198198
// read meta information in the segment to determine the id of this segment
199199
auto info = static_cast<o2::utils::ShmMetaInfo*>(addr);
200-
const int segmentcounter = info->counter.fetch_add(1);
200+
const int npools = wholeRegion ? (info->allocedbytes - sizeof(ShmMetaInfo)) / SHMPOOLSIZE : 1;
201+
const int segmentcounter = info->counter.fetch_add(npools);
201202
LOG(info) << "SEGMENTCOUNT " << segmentcounter;
202203

203204
const auto offset_in_bytes = sizeof(ShmMetaInfo) + segmentcounter * SHMPOOLSIZE;
204205
mBufferPtr = (void*)(((char*)addr) + offset_in_bytes);
206+
mPoolSize = npools * SHMPOOLSIZE;
205207

206-
assert((unsigned long long)((char*)mBufferPtr - (char*)addr) + SHMPOOLSIZE <= info->allocedbytes);
208+
assert((unsigned long long)((char*)mBufferPtr - (char*)addr) + mPoolSize <= info->allocedbytes);
207209

208-
boostmanagedbuffer = new boost::interprocess::wmanaged_external_buffer(create_only, mBufferPtr, SHMPOOLSIZE);
210+
boostmanagedbuffer = new boost::interprocess::wmanaged_external_buffer(create_only, mBufferPtr, mPoolSize);
209211
boostallocator = new boost::interprocess::allocator<char, wmanaged_external_buffer::segment_manager>(
210212
boostmanagedbuffer->get_segment_manager());
211213

@@ -225,6 +227,7 @@ ShmManager::~ShmManager()
225227
void* ShmManager::getmemblock(size_t size)
226228
{
227229
void* addr = nullptr;
230+
std::lock_guard<std::mutex> lock(mAllocMutex);
228231
try {
229232
addr = (void*)boostallocator->allocate(size).get();
230233
} catch (const std::exception& e) {
@@ -235,6 +238,7 @@ void* ShmManager::getmemblock(size_t size)
235238

236239
void ShmManager::freememblock(void* ptr, size_t s)
237240
{
241+
std::lock_guard<std::mutex> lock(mAllocMutex);
238242
boostallocator->deallocate((char*)ptr, s);
239243
}
240244

‎Detectors/3rdparty/FairRootBase/include/FairMCApplication.h‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ enum class FairMCApplicationState
5757
* @since 12.01.04
5858
*/
5959

60+
class FairModule;
6061
class FairMCApplication : public TVirtualMCApplication
6162
{
6263
public:
@@ -304,16 +305,24 @@ class FairMCApplication : public TVirtualMCApplication
304305

305306
ClassDef(FairMCApplication, 4);
306307

307-
private:
308-
/** Protected copy constructor */
308+
protected:
309+
/** Copy constructor, used to create the application of a worker thread */
309310
FairMCApplication(const FairMCApplication&);
311+
312+
private:
310313
/** Protected assignment operator */
311314
FairMCApplication& operator=(const FairMCApplication&);
312315

313316
FairRunInfo fRunInfo; //!
314317
Bool_t fGeometryIsInitialized;
315318

316319
static FairMCApplication* fgMasterInstance;
320+
321+
protected:
322+
/** True for the application on the master thread, false for a worker clone */
323+
Bool_t IsMasterApplication() const { return !fgMasterInstance || fgMasterInstance == this; }
324+
/** The worker clone of a module of the master application (the module itself on the master) */
325+
FairModule* OwnModule(FairModule* module) const;
317326
};
318327

319328
// inline functions

‎Detectors/3rdparty/FairRootBase/include/FairModule.h‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,10 @@ class FairModule : public TNamed
159159
Bool_t fGeoSaved; //! flag for initialisation
160160
TVirtualMC* fMC; //! cahed pointer to MC (available only after initialization)
161161

162+
public:
163+
/** Sets the cached MC, for a module cloned for a worker thread before its MC existed */
164+
void SetMC(TVirtualMC* mc) { fMC = mc; }
165+
162166
ClassDef(FairModule, 4);
163167
};
164168

‎Detectors/3rdparty/FairRootBase/src/FairMCApplication.cxx‎

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -613,6 +613,20 @@ TVirtualMCApplication* FairMCApplication::CloneForWorker() const
613613
return workerApplication;
614614
}
615615

616+
//_____________________________________________________________________________
617+
FairModule* FairMCApplication::OwnModule(FairModule* module) const
618+
{
619+
// the worker's clone of a module, found by name
620+
if (!module || IsMasterApplication() || fModules->IndexOf(module) >= 0) {
621+
return module;
622+
}
623+
auto clone = static_cast<FairModule*>(fModules->FindObject(module->GetName()));
624+
if (!clone) {
625+
LOG(fatal) << "FairMCApplication: no worker clone for module " << module->GetName();
626+
}
627+
return clone;
628+
}
629+
616630
//_____________________________________________________________________________
617631
void FairMCApplication::InitOnWorker()
618632
{
@@ -1014,7 +1028,8 @@ void FairMCApplication::InitGeometry()
10141028
while ((obj = fModIter->Next())) {
10151029
detector = dynamic_cast<FairDetector*>(obj);
10161030
module = dynamic_cast<FairModule*>(obj);
1017-
if (module) {
1031+
if (module && IsMasterApplication()) {
1032+
// media cuts are set once on the master; multithreaded Geant4 workers share them
10181033
module->SetSpecialPhysicsCuts();
10191034
}
10201035
if (detector) {
@@ -1101,21 +1116,30 @@ void FairMCApplication::InitGeometry()
11011116
}
11021117
FairVolume* fNewV = new FairVolume(fv->GetName(), id);
11031118
fNewV->setModId(fv->getModId());
1104-
fNewV->SetModule(fv->GetModule());
1119+
fNewV->SetModule(OwnModule(fv->GetModule()));
11051120
fNewV->setCopyNo(fN->GetNumber());
11061121
fNewV->setMCid(id);
11071122
fVolMap.insert(pair<Int_t, FairVolume*>(id, fNewV));
11081123
}
11091124
} else {
11101125
FairVolume* fNewV = new FairVolume(fv->GetName(), id);
11111126
fNewV->setModId(fv->getModId());
1112-
fNewV->SetModule(fv->GetModule());
1127+
fNewV->SetModule(OwnModule(fv->GetModule()));
11131128
fNewV->setCopyNo(1);
11141129
fNewV->setMCid(id);
11151130
fVolMap.insert(pair<Int_t, FairVolume*>(id, fNewV));
11161131
}
1117-
} else {
1132+
} else if (IsMasterApplication()) {
11181133
fVolMap.insert(pair<Int_t, FairVolume*>(id, fv));
1134+
} else {
1135+
// the volume object is shared with the master; a worker needs its own, pointing at its own module
1136+
FairVolume* fNewV = new FairVolume(fv->GetName(), id);
1137+
fNewV->setModId(fv->getModId());
1138+
fNewV->SetModule(OwnModule(fv->GetModule()));
1139+
fNewV->setCopyNo(fv->getCopyNo());
1140+
fNewV->setMCid(id);
1141+
fNewV->setGeoNode(fv->getGeoNode());
1142+
fVolMap.insert(pair<Int_t, FairVolume*>(id, fNewV));
11191143
}
11201144
} // end off loop Fill sensitive volumes
11211145

‎Detectors/Base/CMakeLists.txt‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ o2_add_library(DetectorsBase
3535
src/Stack.cxx
3636
src/VMCSeederService.cxx
3737
src/SimRandom.cxx
38+
src/MTWorkerHooks.cxx
3839
src/GlobalParams.cxx
3940
src/O2Tessellated.cxx
4041
src/TGeoGeometryUtils.cxx
@@ -97,6 +98,13 @@ o2_add_test(
9798
PUBLIC_LINK_LIBRARIES O2::DetectorsBase
9899
LABELS detectorsbase)
99100

101+
o2_add_test(
102+
DetImplClone
103+
SOURCES test/testDetImplClone.cxx
104+
COMPONENT_NAME DetectorsBase
105+
PUBLIC_LINK_LIBRARIES O2::DetectorsBase
106+
LABELS detectorsbase)
107+
100108
o2_add_test(
101109
O2Tessellated
102110
SOURCES test/testO2Tessellated.cxx

‎Detectors/Base/include/DetectorsBase/Detector.h‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,9 @@ class Detector : public FairDetector
219219
// ... for things that should be setup in each simulation worker separately)
220220
virtual void initializeLate() = 0;
221221

222+
/// whether this detector may run in multithreaded Geant4, with one clone per worker thread
223+
virtual bool isMTSafe() const { return false; }
224+
222225
/// helper wrapper function to register a geometry volume given by name with FairRoot
223226
/// @returns The MonteCarlo ID for the volume
224227
int registerSensitiveVolumeAndGetVolID(std::string const& name);
@@ -334,6 +337,10 @@ class DetImpl : public o2::base::Detector
334337
// offer same constructors as base
335338
using Detector::Detector;
336339

340+
/// copy for a worker thread of multithreaded Geant4; the copy creates its own hit buffers in initializeLate
341+
DetImpl(const DetImpl& rhs) : Detector(rhs) {}
342+
DetImpl& operator=(const DetImpl&) = delete;
343+
337344
// default implementation for getHitBranchNames
338345
std::string getHitBranchNames(int probe) const override
339346
{
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2+
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3+
// All rights not expressly granted are reserved.
4+
//
5+
// This software is distributed under the terms of the GNU General Public
6+
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7+
//
8+
// In applying this license CERN does not waive the privileges and immunities
9+
// granted to it by virtue of its status as an Intergovernmental Organization
10+
// or submit itself to any jurisdiction.
11+
12+
#ifndef ALICEO2_BASE_MTWORKERHOOKS_H
13+
#define ALICEO2_BASE_MTWORKERHOOKS_H
14+
15+
#include <functional>
16+
#include <memory>
17+
#include <fairmq/FwdDecls.h>
18+
19+
class TVirtualMCDecayer;
20+
21+
namespace o2::data
22+
{
23+
class PrimaryChunk;
24+
}
25+
26+
namespace o2::base
27+
{
28+
29+
/// What the simulation setup provides to the transport threads of multithreaded Geant4.
30+
/// Installed once on the master thread before transport starts; each function is called on a worker thread.
31+
struct MTWorkerHooks {
32+
/// Returns the next chunk of primaries for the calling thread, or nullptr when there is no more work
33+
std::function<std::unique_ptr<o2::data::PrimaryChunk>()> fetchChunk;
34+
/// Returns the calling thread's channel to the hit merger
35+
std::function<fair::mq::Channel*()> dataChannel;
36+
/// Creates the external decayer of the calling thread (empty if there is none)
37+
std::function<TVirtualMCDecayer*()> makeDecayer;
38+
/// Ends the event loop of the calling thread after the current event
39+
std::function<void()> stopWorker;
40+
/// Called on a worker thread at the start of each run, after its user actions exist
41+
std::function<void()> beginRunOnWorker;
42+
/// Called once on the master thread after initialisation and before the first transport
43+
std::function<void()> beforeTransport;
44+
/// Called on a worker thread at the start of each track
45+
std::function<void()> beginTrack;
46+
47+
static MTWorkerHooks& instance();
48+
};
49+
50+
} // namespace o2::base
51+
52+
#endif
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2019-2026 CERN and copyright holders of ALICE O2.
2+
// See https://alice-o2.web.cern.ch/copyright for details of the copyright holders.
3+
// All rights not expressly granted are reserved.
4+
//
5+
// This software is distributed under the terms of the GNU General Public
6+
// License v3 (GPL Version 3), copied verbatim in the file "COPYING".
7+
//
8+
// In applying this license CERN does not waive the privileges and immunities
9+
// granted to it by virtue of its status as an Intergovernmental Organization
10+
// or submit itself to any jurisdiction.
11+
12+
#include "DetectorsBase/MTWorkerHooks.h"
13+
14+
namespace o2::base
15+
{
16+
17+
MTWorkerHooks& MTWorkerHooks::instance()
18+
{
19+
static MTWorkerHooks hooks;
20+
return hooks;
21+
}
22+
23+
} // namespace o2::base

‎Detectors/Base/src/Stack.cxx‎

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,7 @@ Stack::Stack(const Stack& rhs)
120120
: FairGenericStack(rhs),
121121
mStack(),
122122
mParticles(),
123+
mTrackIDtoParticlesEntry(rhs.mTrackIDtoParticlesEntry.size(), -1),
123124
mTracks(nullptr),
124125
mIndexMap(),
125126
mIndexOfCurrentTrack(-1),
@@ -131,10 +132,15 @@ Stack::Stack(const Stack& rhs)
131132
mNumberOfPrimariesPopped(0),
132133
mStoreMothers(rhs.mStoreMothers),
133134
mStoreSecondaries(rhs.mStoreSecondaries),
135+
mPruneKinematics(rhs.mPruneKinematics),
134136
mMinHits(rhs.mMinHits),
135137
mEnergyCut(rhs.mEnergyCut),
136-
mTrackRefs(new std::vector<o2::TrackReference>),
137-
mIsG4Like(rhs.mIsG4Like)
138+
mCleanupThreshold(rhs.mCleanupThreshold),
139+
mIsG4Like(rhs.mIsG4Like),
140+
mIsExternalMode(rhs.mIsExternalMode),
141+
mDoTrackSeeding(rhs.mDoTrackSeeding),
142+
mTransportPrimary(rhs.mTransportPrimary),
143+
mTrackRefs(new std::vector<o2::TrackReference>)
138144
{
139145
LOG(debug) << "copy constructor called";
140146
mTracks = new std::vector<MCTrack>();

0 commit comments

Comments
 (0)