Skip to content

Commit 0d6f96a

Browse files
Tristan Wenzelsawenzel
authored andcommitted
Multi-threaded material budget LUT creation
Introduces multi-threaded creation of the material budget lookup table, reducing it from hours to minutes. Creating the LUT walks every cell of every layer through TGeo and the cells are independent, so they are spread over TBB tasks with one TGeoNavigator per thread. The innermost 20 layers at 60 trials/cell drop from 28 min to 72 s on 28 cores. Effective only with ROOT >= v6-36-10-alice3, which removes a per-query thread-id lookup and the false sharing between per-thread scratch buffers of TGeo shapes. On older ROOT the parallel path is correct, just slower -- it saturates near 12x. All layers map onto a single flat cell index so the load stays balanced despite very different cell counts per layer; a binary search maps a flat index back to (layer, iz, iphi). The worker navigators are given back at the end. meanMaterialBudget() takes an optional navigator: a caller passing its own runs lock-free, a caller passing none shares gGeoManager's and still takes the mutex. Deciding from the argument keeps it local, so process-global state cannot break it. Thread count comes from the new populateFromTGeo() argument, falling back to NTHREADS_MATBUD; the default is the previous serial path. Results are independent of the thread count -- compareMatBudLUT.C checks a parallel LUT against a serial one cell by cell, and they match exactly over all 129523 cells. Supervised-by: Sandro Wenzel <sandro.wenzel@cern.ch>
1 parent 980546b commit 0d6f96a

10 files changed

Lines changed: 260 additions & 26 deletions

File tree

Detectors/Base/CMakeLists.txt

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,7 @@ if(BUILD_SIMULATION)
9292
endif()
9393

9494
install(FILES test/buildMatBudLUT.C
95+
test/compareMatBudLUT.C
9596
test/extractLUTLayers.C
9697
test/rescaleLUT.C
9798
DESTINATION share/macro/)
@@ -100,6 +101,10 @@ o2_add_test_root_macro(test/buildMatBudLUT.C
100101
PUBLIC_LINK_LIBRARIES O2::DetectorsBase
101102
LABELS detectorsbase)
102103

104+
o2_add_test_root_macro(test/compareMatBudLUT.C
105+
PUBLIC_LINK_LIBRARIES O2::DetectorsBase
106+
LABELS detectorsbase)
107+
103108
o2_add_test_root_macro(test/extractLUTLayers.C
104109
PUBLIC_LINK_LIBRARIES O2::DetectorsBase
105110
LABELS detectorsbase)

Detectors/Base/include/DetectorsBase/GeometryManager.h

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
#include <mutex>
3030
class TGeoHMatrix; // lines 11-11
3131
class TGeoManager; // lines 9-9
32+
class TGeoNavigator;
3233

3334
namespace o2
3435
{
@@ -96,14 +97,18 @@ class GeometryManager : public TObject
9697
ClassDefNV(MatBudgetExt, 1);
9798
};
9899

99-
static o2::base::MatBudget meanMaterialBudget(float x0, float y0, float z0, float x1, float y1, float z1);
100-
static o2::base::MatBudget meanMaterialBudget(const math_utils::Point3D<float>& start, const math_utils::Point3D<float>& end)
100+
/// Mean material budget between two points. Pass a navigator owned by the calling thread to
101+
/// run lock-free from several threads; with nav = nullptr the shared navigator is used under
102+
/// a mutex, as before.
103+
static o2::base::MatBudget meanMaterialBudget(float x0, float y0, float z0, float x1, float y1, float z1,
104+
TGeoNavigator* nav = nullptr);
105+
static o2::base::MatBudget meanMaterialBudget(const math_utils::Point3D<float>& start, const math_utils::Point3D<float>& end, TGeoNavigator* nav = nullptr)
101106
{
102-
return meanMaterialBudget(start.X(), start.Y(), start.Z(), end.X(), end.Y(), end.Z());
107+
return meanMaterialBudget(start.X(), start.Y(), start.Z(), end.X(), end.Y(), end.Z(), nav);
103108
}
104-
static o2::base::MatBudget meanMaterialBudget(const math_utils::Point3D<double>& start, const math_utils::Point3D<double>& end)
109+
static o2::base::MatBudget meanMaterialBudget(const math_utils::Point3D<double>& start, const math_utils::Point3D<double>& end, TGeoNavigator* nav = nullptr)
105110
{
106-
return meanMaterialBudget(start.X(), start.Y(), start.Z(), end.X(), end.Y(), end.Z());
111+
return meanMaterialBudget(start.X(), start.Y(), start.Z(), end.X(), end.Y(), end.Z(), nav);
107112
}
108113

109114
static MatBudgetExt meanMaterialBudgetExt(float x0, float y0, float z0, float x1, float y1, float z1);

Detectors/Base/include/DetectorsBase/MatLayerCyl.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525
#include "GPUCommonMath.h"
2626
#include "DetectorsBase/MatCell.h"
2727

28+
class TGeoNavigator;
29+
2830
namespace o2
2931
{
3032
namespace base
@@ -66,7 +68,7 @@ class MatLayerCyl : public o2::gpu::FlatObject
6668
void initSegmentation(float rMin, float rMax, float zHalfSpan, int nz, int nphi);
6769
void initSegmentation(float rMin, float rMax, float zHalfSpan, float dzMin, float drphiMin);
6870
void populateFromTGeo(int ntrPerCell = 10);
69-
void populateFromTGeo(int ip, int iz, int ntrPerCell);
71+
void populateFromTGeo(int ip, int iz, int ntrPerCell, TGeoNavigator* nav = nullptr);
7072
void print(bool data = false) const;
7173
#endif // !GPUCA_ALIGPUCODE
7274

Detectors/Base/include/DetectorsBase/MatLayerCylSet.h

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,10 @@ class MatLayerCylSet : public o2::gpu::FlatObject
7373
#ifndef GPUCA_ALIGPUCODE // this part is unvisible on GPU version
7474
void print(bool data = false) const;
7575
void addLayer(float rmin, float rmax, float zmax, float dz, float drphi);
76-
void populateFromTGeo(int ntrPerCel = 10);
76+
/// Populate the LUT from TGeo. nThreads > 1 fills the cells in parallel (one TGeoNavigator
77+
/// per thread); nThreads < 0 takes the count from the NTHREADS_MATBUD environment variable.
78+
void populateFromTGeo(int ntrPerCel = 10, int nThreads = -1);
79+
static int getNThreadsFromEnv();
7780
void optimizePhiSlices(float maxRelDiff = 0.05);
7881

7982
void dumpToTree(const std::string& outName = "matbudTree.root") const;

Detectors/Base/src/GeometryManager.cxx

Lines changed: 22 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
#include <TCollection.h> // for TIter
1717
#include <TFile.h>
1818
#include <TGeoMatrix.h> // for TGeoHMatrix
19+
#include <TGeoNavigator.h> // for TGeoNavigator
1920
#include <TGeoNode.h> // for TGeoNode
2021
#include <TGeoPhysicalNode.h> // for TGeoPhysicalNode, TGeoPNEntry
2122
#include <string>
@@ -398,7 +399,8 @@ GeometryManager::MatBudgetExt GeometryManager::meanMaterialBudgetExt(float x0, f
398399
}
399400

400401
//_____________________________________________________________________________________
401-
o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, float z0, float x1, float y1, float z1)
402+
o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, float z0, float x1, float y1, float z1,
403+
TGeoNavigator* nav)
402404
{
403405
//
404406
// Calculate mean material budget and material properties between
@@ -414,6 +416,8 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa
414416
//
415417
// Ported to O2: ruben.shahoyan@cern.ch
416418
//
419+
// Multi-threaded execution: pass a navigator owned by the calling thread.
420+
//
417421

418422
double length, startD[3] = {x0, y0, z0};
419423
double dir[3] = {x1 - x0, y1 - y0, z1 - z0};
@@ -425,9 +429,17 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa
425429
for (int i = 3; i--;) {
426430
dir[i] *= invlen;
427431
}
428-
std::lock_guard<std::mutex> guard(sTGMutex);
432+
// A caller that passes its own navigator owns it exclusively, so no locking is needed. A caller
433+
// that passes none shares gGeoManager's current navigator and must still serialize. Deciding
434+
// this from the argument keeps the choice local: it does not depend on -- and cannot be broken
435+
// by -- process-global state such as TGeoManager::GetMaxThreads().
436+
std::unique_lock<std::mutex> guard(sTGMutex, std::defer_lock);
437+
if (!nav) {
438+
guard.lock();
439+
nav = gGeoManager->GetCurrentNavigator();
440+
}
429441
// Initialize start point and direction
430-
TGeoNode* currentnode = gGeoManager->InitTrack(startD, dir);
442+
TGeoNode* currentnode = nav->InitTrack(startD, dir);
431443
if (!currentnode) {
432444
LOG(error) << "start point out of geometry: " << x0 << ':' << y0 << ':' << z0;
433445
return o2::base::MatBudget(); // return empty struct
@@ -439,11 +451,11 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa
439451

440452
// Locate next boundary within length without computing safety.
441453
// Propagate either with length (if no boundary found) or just cross boundary
442-
gGeoManager->FindNextBoundaryAndStep(length, kFALSE);
454+
nav->FindNextBoundaryAndStep(length, kFALSE);
443455
Double_t stepTot = 0.0; // Step made
444-
Double_t step = gGeoManager->GetStep();
456+
Double_t step = nav->GetStep();
445457
// If no boundary within proposed length, return current step data
446-
if (!gGeoManager->IsOnBoundary()) {
458+
if (!nav->IsOnBoundary()) {
447459
budStep.meanX2X0 = budStep.length / budStep.meanX2X0;
448460
return o2::base::MatBudget(budStep);
449461
}
@@ -458,7 +470,7 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa
458470
if (nzero > 3) {
459471
// This means navigation has problems on one boundary
460472
// Try to cross by making a small step
461-
const double* curPos = gGeoManager->GetCurrentPoint();
473+
const double* curPos = nav->GetCurrentPoint();
462474
LOG(warning) << "Cannot cross boundary at (" << curPos[0] << ',' << curPos[1] << ',' << curPos[2] << ')';
463475
budTotal.meanRho /= stepTot;
464476
budTotal.length = stepTot;
@@ -472,14 +484,14 @@ o2::base::MatBudget GeometryManager::meanMaterialBudget(float x0, float y0, floa
472484
if (step >= length) {
473485
break;
474486
}
475-
currentnode = gGeoManager->GetCurrentNode();
487+
currentnode = nav->GetCurrentNode();
476488
if (!currentnode) {
477489
break;
478490
}
479491
length -= step;
480492
accountMaterial(currentnode->GetVolume()->GetMedium()->GetMaterial(), budStep);
481-
gGeoManager->FindNextBoundaryAndStep(length, kFALSE);
482-
step = gGeoManager->GetStep();
493+
nav->FindNextBoundaryAndStep(length, kFALSE);
494+
step = nav->GetStep();
483495
}
484496
budTotal.meanRho /= stepTot;
485497
budTotal.length = stepTot;

Detectors/Base/src/MatLayerCyl.cxx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ void MatLayerCyl::populateFromTGeo(int ntrPerCell)
123123
}
124124

125125
//________________________________________________________________________________
126-
void MatLayerCyl::populateFromTGeo(int ip, int iz, int ntrPerCell)
126+
void MatLayerCyl::populateFromTGeo(int ip, int iz, int ntrPerCell, TGeoNavigator* nav)
127127
{
128128
/// populate cell with info extracted from TGeometry, using ntrPerCell test tracks per cell
129129

@@ -136,7 +136,7 @@ void MatLayerCyl::populateFromTGeo(int ip, int iz, int ntrPerCell)
136136
float dzt = zs > 0.f ? 0.25 * dz : -0.25 * dz; // to avoid 90 degree polar angle
137137
for (int isp = ntrPerCell; isp--;) {
138138
o2::math_utils::sincos(phmn + (isp + 0.5) * getDPhi() / ntrPerCell, sn, cs);
139-
auto bud = o2::base::GeometryManager::meanMaterialBudget(rMin * cs, rMin * sn, zs - dzt, rMax * cs, rMax * sn, zs + dzt);
139+
auto bud = o2::base::GeometryManager::meanMaterialBudget(rMin * cs, rMin * sn, zs - dzt, rMax * cs, rMax * sn, zs + dzt, nav);
140140
if (bud.length > 0.) {
141141
meanRho += bud.length * bud.meanRho;
142142
meanX2X0 += bud.meanX2X0; // we store actually not X2X0 but 1./X0

Detectors/Base/src/MatLayerCylSet.cxx

Lines changed: 104 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,16 @@
1717
#ifndef GPUCA_ALIGPUCODE // this part is unvisible on GPU version
1818
#include "GPUCommonLogger.h"
1919
#include <TFile.h>
20+
#include <TGeoManager.h>
2021
#include "CommonUtils/TreeStreamRedirector.h"
22+
#include <tbb/blocked_range.h>
23+
#include <tbb/enumerable_thread_specific.h>
24+
#include <tbb/global_control.h>
25+
#include <tbb/parallel_for.h>
26+
#include <algorithm>
27+
#include <chrono>
28+
#include <cstdlib>
29+
#include <vector>
2130
//#define _DBG_LOC_ // for local debugging only
2231

2332
#endif // !GPUCA_ALIGPUCODE
@@ -69,9 +78,26 @@ void MatLayerCylSet::addLayer(float rmin, float rmax, float zmax, float dz, floa
6978
}
7079

7180
//________________________________________________________________________________
72-
void MatLayerCylSet::populateFromTGeo(int ntrPerCell)
81+
int MatLayerCylSet::getNThreadsFromEnv()
7382
{
74-
///< populate layers, using ntrPerCell test tracks per cell
83+
///< number of threads requested via NTHREADS_MATBUD, or 1 if unset/invalid
84+
const char* env = std::getenv("NTHREADS_MATBUD");
85+
if (!env) {
86+
return 1;
87+
}
88+
int n = std::atoi(env);
89+
if (n < 1) {
90+
LOG(warning) << "Ignoring invalid NTHREADS_MATBUD=" << env;
91+
return 1;
92+
}
93+
return n;
94+
}
95+
96+
//________________________________________________________________________________
97+
void MatLayerCylSet::populateFromTGeo(int ntrPerCell, int nThreads)
98+
{
99+
///< populate layers, using ntrPerCell test tracks per cell.
100+
///< nThreads < 0 takes the number of threads from the NTHREADS_MATBUD environment variable.
75101
assert(mConstructionMask == InProgress);
76102

77103
int nlr = getNLayers();
@@ -83,12 +109,86 @@ void MatLayerCylSet::populateFromTGeo(int ntrPerCell)
83109
LOG(error) << "The LUT is already populated";
84110
return;
85111
}
112+
113+
if (nThreads < 0) {
114+
nThreads = getNThreadsFromEnv();
115+
}
116+
117+
using Clock = std::chrono::steady_clock;
118+
auto seconds = [](Clock::time_point a, Clock::time_point b) {
119+
return std::chrono::duration<double>(b - a).count();
120+
};
121+
122+
if (nThreads <= 1) {
123+
for (int i = 0; i < nlr; i++) {
124+
LOG(info) << "Populating with " << ntrPerCell << " trials Lr " << i;
125+
get()->mLayers[i].print();
126+
}
127+
const auto tFillStart = Clock::now();
128+
for (int i = 0; i < nlr; i++) {
129+
get()->mLayers[i].populateFromTGeo(ntrPerCell);
130+
}
131+
const auto tFillEnd = Clock::now();
132+
finalizeStructures();
133+
LOG(info) << "LUT fill: 1 thread, cells " << seconds(tFillStart, tFillEnd) << " s";
134+
return;
135+
}
136+
137+
// Cells of all layers form one flat index range so that the load is balanced across
138+
// threads even though layers differ a lot in cell count. layerOffsets[i] is the first
139+
// flat index of layer i; a binary search maps a flat index back to (layer, iz, iphi).
140+
std::vector<size_t> layerOffsets(nlr + 1, 0);
86141
for (int i = 0; i < nlr; i++) {
87-
printf("Populating with %d trials Lr %3d ", ntrPerCell, i);
142+
LOG(info) << "Queuing " << ntrPerCell << " trials Lr " << i;
88143
get()->mLayers[i].print();
89-
get()->mLayers[i].populateFromTGeo(ntrPerCell);
144+
const auto& lr = get()->mLayers[i];
145+
layerOffsets[i + 1] = layerOffsets[i] + size_t(lr.getNZBins()) * lr.getNPhiBins();
90146
}
147+
const size_t totalCells = layerOffsets[nlr];
148+
149+
const auto tSetupStart = Clock::now();
150+
151+
// TGeo has to be told that several threads will navigate it, and each thread needs its own
152+
// navigator. SetMaxThreads() is one-way -- TGeoManager has no API to return to
153+
// single-threaded mode -- so we do not pretend to restore it; that is harmless because
154+
// meanMaterialBudget() decides whether to lock from its own argument, not from this global.
155+
// The navigators we book are ours, though, so those we do give back.
156+
gGeoManager->SetMaxThreads(nThreads);
157+
158+
tbb::enumerable_thread_specific<TGeoNavigator*> threadNavigators(
159+
[]() { return gGeoManager->AddNavigator(); });
160+
161+
const auto tFillStart = Clock::now();
162+
{
163+
tbb::global_control threadControl(tbb::global_control::max_allowed_parallelism, nThreads);
164+
tbb::parallel_for(tbb::blocked_range<size_t>(0, totalCells),
165+
[this, ntrPerCell, &layerOffsets, &threadNavigators](const tbb::blocked_range<size_t>& range) {
166+
TGeoNavigator* nav = threadNavigators.local();
167+
for (size_t idx = range.begin(); idx != range.end(); ++idx) {
168+
auto it = std::upper_bound(layerOffsets.begin(), layerOffsets.end(), idx);
169+
const int layerIdx = int(std::distance(layerOffsets.begin(), it)) - 1;
170+
const size_t cellInLayer = idx - layerOffsets[layerIdx];
171+
auto& layer = this->get()->mLayers[layerIdx];
172+
const int nphi = layer.getNPhiBins();
173+
layer.populateFromTGeo(int(cellInLayer % nphi), int(cellInLayer / nphi), ntrPerCell, nav);
174+
}
175+
});
176+
}
177+
178+
const auto tFillEnd = Clock::now();
179+
180+
for (TGeoNavigator* nav : threadNavigators) {
181+
gGeoManager->RemoveNavigator(nav);
182+
}
183+
91184
finalizeStructures();
185+
const auto tEnd = Clock::now();
186+
187+
// Reported separately because only the middle term scales: the setup walks every volume
188+
// in the geometry (TGeoManager::SetMaxThreads) and the teardown is serial by nature.
189+
LOG(info) << "LUT fill: " << nThreads << " threads, setup " << seconds(tSetupStart, tFillStart)
190+
<< " s, cells " << seconds(tFillStart, tFillEnd)
191+
<< " s, finalize " << seconds(tFillEnd, tEnd) << " s";
92192
}
93193

94194
//________________________________________________________________________________

Detectors/Base/test/README.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,16 @@ root -b -q O2/Detectors/Base/test/buildMatBudLUT.C+
1313

1414
The generation is quite time consuming (may take ~30 min).
1515

16+
It can be filled in parallel, one `TGeoNavigator` per thread, by passing a thread count as the
17+
5th argument of `buildMatBudLUT` or by setting the environment variable:
18+
```
19+
export NTHREADS_MATBUD=16
20+
```
21+
The result does not depend on the number of threads. Scaling beyond a few threads needs
22+
ROOT >= v6-36-10-alice3, which removes the per-query thread-id lookup and the false sharing
23+
between the per-thread scratch buffers of TGeo shapes; with older ROOT the parallel path is
24+
still correct, just slower.
25+
1626
The optimized LUT will be stored in the matbud.root file.
1727

1828
Load it as:

Detectors/Base/test/buildMatBudLUT.C

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,10 @@ o2::base::MatLayerCylSet mbLUT;
2727

2828
bool testMBLUT(const std::string& lutFile = "matbud.root");
2929

30-
bool buildMatBudLUT(int nTst = 60, int maxLr = -1, const std::string& outFile = "matbud.root", const std::string& geomName = "o2sim_geometry-aligned.root");
30+
/// Build the material budget LUT. nThreads < 0 takes the thread count from NTHREADS_MATBUD.
31+
bool buildMatBudLUT(int nTst = 60, int maxLr = -1, const std::string& outFile = "matbud.root",
32+
const std::string& geomNamePrefix = "o2sim", const std::string& opts = "",
33+
int nThreads = -1);
3134

3235
struct LrData {
3336
float rMin = 0.f;
@@ -42,7 +45,8 @@ struct LrData {
4245
std::vector<LrData> lrData;
4346
void configLayers();
4447

45-
bool buildMatBudLUT(int nTst, int maxLr, const std::string& outFile, const std::string& geomNamePrefix, const std::string& opts)
48+
bool buildMatBudLUT(int nTst, int maxLr, const std::string& outFile, const std::string& geomNamePrefix,
49+
const std::string& opts, int nThreads)
4650
{
4751
auto geomName = o2::base::NameConf::getGeomFileName(geomNamePrefix);
4852
if (gSystem->AccessPathName(geomName.c_str())) { // if needed, create geometry
@@ -67,7 +71,7 @@ bool buildMatBudLUT(int nTst, int maxLr, const std::string& outFile, const std::
6771
}
6872

6973
TStopwatch sw;
70-
mbLUT.populateFromTGeo(nTst);
74+
mbLUT.populateFromTGeo(nTst, nThreads);
7175
mbLUT.optimizePhiSlices(); // move to populateFromTGeo
7276
mbLUT.flatten(); // move to populateFromTGeo
7377

0 commit comments

Comments
 (0)