Skip to content

Commit 383674d

Browse files
sawenzelclaude
andcommitted
Make the magnetic field evaluation reentrant
This removes the shared scratch state from the Chebyshev field evaluation, so that one field can be evaluated from several threads. - Chebyshev3D keeps the mapped coordinates in a local array instead of a member. - Chebyshev3DCalc evaluates into scratch on the stack, or into a thread-local buffer for parameterisations larger than 64 rows or columns. The ALICE maps use at most 14. - MagneticWrapperChebyshev no longer uses static coordinates in the TPC integrals. - The arithmetic is unchanged, so the field values are bit-identical. - A unit test evaluates the field from 8 threads and compares with a sequential pass. This prepares the simulation for multithreaded Geant4, where all workers share one field. https://its.cern.ch/jira/browse/O2-7305 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e763588 commit 383674d

5 files changed

Lines changed: 144 additions & 48 deletions

File tree

‎Common/Field/src/MagneticWrapperChebyshev.cxx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,7 @@ Int_t MagneticWrapperChebyshev::findTPCRatSegment(const Double_t* rpz) const
690690

691691
void MagneticWrapperChebyshev::getTPCIntegral(const Double_t* xyz, Double_t* b) const
692692
{
693-
static Double_t rphiz[3];
693+
Double_t rphiz[3];
694694

695695
// TPCInt region
696696
// convert coordinates to cyl system
@@ -712,7 +712,7 @@ void MagneticWrapperChebyshev::getTPCIntegral(const Double_t* xyz, Double_t* b)
712712

713713
void MagneticWrapperChebyshev::getTPCRatIntegral(const Double_t* xyz, Double_t* b) const
714714
{
715-
static Double_t rphiz[3];
715+
Double_t rphiz[3];
716716

717717
// TPCRatIntegral region
718718
// convert coordinates to cylindrical system

‎Common/Field/test/testMagneticField.cxx‎

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,10 @@
2121
#include <TFile.h>
2222
#include <TStopwatch.h>
2323
#include <TRandom.h>
24+
#include <algorithm>
25+
#include <array>
26+
#include <thread>
27+
#include <vector>
2428

2529
using namespace o2::field;
2630

@@ -144,3 +148,40 @@ BOOST_AUTO_TEST_CASE(MagneticField_reinitialization_test)
144148
}
145149
}
146150
}
151+
152+
BOOST_AUTO_TEST_CASE(MagneticField_threads)
153+
{
154+
// evaluating one field from several threads at once gives bit-identical values to a sequential pass
155+
MagneticField fld("Maps", "Maps", 1., 1., o2::field::MagFieldParam::k5kG);
156+
constexpr int npoints = 20000;
157+
constexpr int nthreads = 8;
158+
std::vector<std::array<double, 3>> xyz(npoints);
159+
for (auto& p : xyz) {
160+
p = {gRandom->Uniform(-450., 450.), gRandom->Uniform(-450., 450.), gRandom->Uniform(-1500., 1500.)};
161+
}
162+
// field, Bz, TPC integral and TPC ratio integral for every point
163+
constexpr int nval = 10;
164+
auto evaluate = [&fld, &xyz](std::vector<double>& out) {
165+
out.resize(npoints * nval);
166+
for (int i = 0; i < npoints; ++i) {
167+
fld.Field(xyz[i].data(), &out[i * nval]);
168+
out[i * nval + 3] = fld.getBz(xyz[i].data());
169+
fld.getTPCIntegral(xyz[i].data(), &out[i * nval + 4]);
170+
fld.getTPCRatIntegral(xyz[i].data(), &out[i * nval + 7]);
171+
}
172+
};
173+
std::vector<double> reference;
174+
evaluate(reference);
175+
std::vector<std::vector<double>> results(nthreads);
176+
std::vector<std::thread> threads;
177+
for (int t = 0; t < nthreads; ++t) {
178+
threads.emplace_back(evaluate, std::ref(results[t]));
179+
}
180+
for (auto& th : threads) {
181+
th.join();
182+
}
183+
for (int t = 0; t < nthreads; ++t) {
184+
auto diff = std::mismatch(reference.begin(), reference.end(), results[t].begin());
185+
BOOST_CHECK_MESSAGE(diff.first == reference.end(), "thread " << t << " differs at value " << (diff.first - reference.begin()));
186+
}
187+
}

‎Common/MathUtils/include/MathUtils/Chebyshev3D.h‎

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -292,66 +292,72 @@ inline Bool_t Chebyshev3D::isInside(const Double_t* par) const
292292
/// Evaluates Chebyshev parameterization for 3d->DimOut function
293293
inline void Chebyshev3D::Eval(const Float_t* par, Float_t* res)
294294
{
295+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
295296
for (int i = 3; i--;) {
296-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
297+
coef[i] = mapToInternal(par[i], i);
297298
}
298299
for (int i = mOutputArrayDimension; i--;) {
299-
res[i] = getChebyshevCalc(i)->Eval(mTemporaryCoefficient);
300+
res[i] = getChebyshevCalc(i)->Eval(coef);
300301
}
301302
}
302303

303304
/// Evaluates Chebyshev parameterization for 3d->DimOut function
304305
inline void Chebyshev3D::Eval(const Double_t* par, Double_t* res)
305306
{
307+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
306308
for (int i = 3; i--;) {
307-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
309+
coef[i] = mapToInternal(par[i], i);
308310
}
309311
for (int i = mOutputArrayDimension; i--;) {
310-
res[i] = getChebyshevCalc(i)->Eval(mTemporaryCoefficient);
312+
res[i] = getChebyshevCalc(i)->Eval(coef);
311313
}
312314
}
313315

314316
/// Evaluates Chebyshev parameterization for idim-th output dimension of 3d->DimOut function
315317
inline Double_t Chebyshev3D::Eval(const Double_t* par, int idim)
316318
{
319+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
317320
for (int i = 3; i--;) {
318-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
321+
coef[i] = mapToInternal(par[i], i);
319322
}
320-
return getChebyshevCalc(idim)->Eval(mTemporaryCoefficient);
323+
return getChebyshevCalc(idim)->Eval(coef);
321324
}
322325

323326
/// Evaluates Chebyshev parameterization for idim-th output dimension of 3d->DimOut function
324327
inline Float_t Chebyshev3D::Eval(const Float_t* par, int idim)
325328
{
329+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
326330
for (int i = 3; i--;) {
327-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
331+
coef[i] = mapToInternal(par[i], i);
328332
}
329-
return getChebyshevCalc(idim)->Eval(mTemporaryCoefficient);
333+
return getChebyshevCalc(idim)->Eval(coef);
330334
}
331335

332336
/// Returns the gradient matrix
333337
inline void Chebyshev3D::evaluateDerivative3D(const Float_t* par, Float_t dbdr[3][3])
334338
{
339+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
335340
for (int i = 3; i--;) {
336-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
341+
coef[i] = mapToInternal(par[i], i);
337342
}
338343
for (int ib = 3; ib--;) {
339344
for (int id = 3; id--;) {
340-
dbdr[ib][id] = getChebyshevCalc(ib)->evaluateDerivative(id, mTemporaryCoefficient) * mBoundaryMappingScale[id];
345+
dbdr[ib][id] = getChebyshevCalc(ib)->evaluateDerivative(id, coef) * mBoundaryMappingScale[id];
341346
}
342347
}
343348
}
344349

345350
/// Returns the gradient matrix
346351
inline void Chebyshev3D::evaluateDerivative3D2(const Float_t* par, Float_t dbdrdr[3][3][3])
347352
{
353+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
348354
for (int i = 3; i--;) {
349-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
355+
coef[i] = mapToInternal(par[i], i);
350356
}
351357
for (int ib = 3; ib--;) {
352358
for (int id = 3; id--;) {
353359
for (int id1 = 3; id1--;) {
354-
dbdrdr[ib][id][id1] = getChebyshevCalc(ib)->evaluateDerivative2(id, id1, mTemporaryCoefficient) *
360+
dbdrdr[ib][id][id1] = getChebyshevCalc(ib)->evaluateDerivative2(id, id1, coef) *
355361
mBoundaryMappingScale[id] * mBoundaryMappingScale[id1];
356362
}
357363
}
@@ -361,22 +367,24 @@ inline void Chebyshev3D::evaluateDerivative3D2(const Float_t* par, Float_t dbdrd
361367
// Evaluates Chebyshev parameterization derivative for 3d->DimOut function
362368
inline void Chebyshev3D::evaluateDerivative(int dimd, const Float_t* par, Float_t* res)
363369
{
370+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
364371
for (int i = 3; i--;) {
365-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
372+
coef[i] = mapToInternal(par[i], i);
366373
}
367374
for (int i = mOutputArrayDimension; i--;) {
368-
res[i] = getChebyshevCalc(i)->evaluateDerivative(dimd, mTemporaryCoefficient) * mBoundaryMappingScale[dimd];
375+
res[i] = getChebyshevCalc(i)->evaluateDerivative(dimd, coef) * mBoundaryMappingScale[dimd];
369376
};
370377
}
371378

372379
// Evaluates Chebyshev parameterization 2nd derivative over dimd1 and dimd2 dimensions for 3d->DimOut function
373380
inline void Chebyshev3D::evaluateDerivative2(int dimd1, int dimd2, const Float_t* par, Float_t* res)
374381
{
382+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
375383
for (int i = 3; i--;) {
376-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
384+
coef[i] = mapToInternal(par[i], i);
377385
}
378386
for (int i = mOutputArrayDimension; i--;) {
379-
res[i] = getChebyshevCalc(i)->evaluateDerivative2(dimd1, dimd2, mTemporaryCoefficient) *
387+
res[i] = getChebyshevCalc(i)->evaluateDerivative2(dimd1, dimd2, coef) *
380388
mBoundaryMappingScale[dimd1] * mBoundaryMappingScale[dimd2];
381389
}
382390
}
@@ -385,20 +393,22 @@ inline void Chebyshev3D::evaluateDerivative2(int dimd1, int dimd2, const Float_t
385393
/// function
386394
inline Float_t Chebyshev3D::evaluateDerivative(int dimd, const Float_t* par, int idim)
387395
{
396+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
388397
for (int i = 3; i--;) {
389-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
398+
coef[i] = mapToInternal(par[i], i);
390399
}
391-
return getChebyshevCalc(idim)->evaluateDerivative(dimd, mTemporaryCoefficient) * mBoundaryMappingScale[dimd];
400+
return getChebyshevCalc(idim)->evaluateDerivative(dimd, coef) * mBoundaryMappingScale[dimd];
392401
}
393402

394403
/// Evaluates Chebyshev parameterization 2ns derivative over dimd1 and dimd2 dimensions for idim-th output dimension of
395404
/// 3d->DimOut function
396405
inline Float_t Chebyshev3D::evaluateDerivative2(int dimd1, int dimd2, const Float_t* par, int idim)
397406
{
407+
Float_t coef[3]; // mapped arguments, local so that evaluation is reentrant
398408
for (int i = 3; i--;) {
399-
mTemporaryCoefficient[i] = mapToInternal(par[i], i);
409+
coef[i] = mapToInternal(par[i], i);
400410
}
401-
return getChebyshevCalc(idim)->evaluateDerivative2(dimd1, dimd2, mTemporaryCoefficient) *
411+
return getChebyshevCalc(idim)->evaluateDerivative2(dimd1, dimd2, coef) *
402412
mBoundaryMappingScale[dimd1] * mBoundaryMappingScale[dimd2];
403413
}
404414

‎Common/MathUtils/include/MathUtils/Chebyshev3DCalc.h‎

Lines changed: 47 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@
1919
#include <TNamed.h> // for TNamed
2020
#include <cstdio> // for FILE, stdout
2121
#include <cmath> // for std::fma
22+
#include <algorithm>
23+
#include <vector>
2224
#include "Rtypes.h" // for Float_t, UShort_t, Int_t, Double_t, etc
2325

2426
class TString;
@@ -171,6 +173,37 @@ class Chebyshev3DCalc : public TNamed
171173
Double_t Eval(const Double_t* par) const;
172174

173175
private:
176+
/// Scratch arrays for one evaluation: on the stack for the usual sizes, thread-local beyond them,
177+
/// so that evaluation is reentrant. The ALICE field maps use at most 14 rows and columns.
178+
class EvalScratch
179+
{
180+
public:
181+
EvalScratch(int nrows, int ncols)
182+
{
183+
if (nrows <= kStackSize && ncols <= kStackSize) {
184+
mRows = mStackRows;
185+
mCols = mStackCols;
186+
} else {
187+
thread_local std::vector<Float_t> rows, cols;
188+
rows.resize(std::max<size_t>(nrows, rows.size()));
189+
cols.resize(std::max<size_t>(ncols, cols.size()));
190+
mRows = rows.data();
191+
mCols = cols.data();
192+
}
193+
}
194+
EvalScratch(const EvalScratch&) = delete;
195+
EvalScratch& operator=(const EvalScratch&) = delete;
196+
Float_t* rows() { return mRows; }
197+
Float_t* cols() { return mCols; }
198+
199+
private:
200+
static constexpr int kStackSize = 64;
201+
Float_t mStackRows[kStackSize];
202+
Float_t mStackCols[kStackSize];
203+
Float_t* mRows;
204+
Float_t* mCols;
205+
};
206+
174207
Int_t mNumberOfCoefficients; ///< total number of coeeficients
175208
Int_t mNumberOfRows; ///< number of significant rows in the 3D coeffs matrix
176209
Int_t mNumberOfColumns; ///< max number of significant cols in the 3D coeffs matrix
@@ -188,8 +221,8 @@ class Chebyshev3DCalc : public TNamed
188221
// coeffs for col/row
189222
Float_t* mCoefficients; //[mNumberOfCoefficients] array of Chebyshev coefficients
190223

191-
Float_t* mTemporaryCoefficients2D; //[mNumberOfColumns] temp. coeffs for 2d summation
192-
Float_t* mTemporaryCoefficients1D; //[mNumberOfRows] temp. coeffs for 1d summation
224+
Float_t* mTemporaryCoefficients2D; //[mNumberOfColumns] no longer used for evaluation
225+
Float_t* mTemporaryCoefficients1D; //[mNumberOfRows] no longer used for evaluation
193226

194227
ClassDefOverride(o2::math_utils::Chebyshev3DCalc,
195228
2) // Class for interpolation of 3D->1 function by Chebyshev parametrization
@@ -223,32 +256,38 @@ inline Float_t Chebyshev3DCalc::chebyshevEvaluation1D(Float_t x, const Float_t*
223256
/// VERY IMPORTANT: par must contain the function arguments ALREADY MAPPED to [-1:1] interval
224257
inline Float_t Chebyshev3DCalc::Eval(const Float_t* par) const
225258
{
259+
EvalScratch scratch(mNumberOfRows, mNumberOfColumns);
260+
Float_t* coefs2D = scratch.cols();
261+
Float_t* coefs1D = scratch.rows();
226262
for (int id0 = mNumberOfRows; id0--;) {
227263
int nCLoc = mNumberOfColumnsAtRow[id0]; // number of significant coefs on this row
228264
int col0 = mColumnAtRowBeginning[id0]; // beginning of local column in the 2D boundary matrix
229265
for (int id1 = nCLoc; id1--;) {
230266
int id = id1 + col0;
231-
mTemporaryCoefficients2D[id1] = chebyshevEvaluation1D(par[2], mCoefficients + mCoefficientBound2D1[id], mCoefficientBound2D0[id]);
267+
coefs2D[id1] = chebyshevEvaluation1D(par[2], mCoefficients + mCoefficientBound2D1[id], mCoefficientBound2D0[id]);
232268
}
233-
mTemporaryCoefficients1D[id0] = chebyshevEvaluation1D(par[1], mTemporaryCoefficients2D, nCLoc);
269+
coefs1D[id0] = chebyshevEvaluation1D(par[1], coefs2D, nCLoc);
234270
}
235-
return chebyshevEvaluation1D(par[0], mTemporaryCoefficients1D, mNumberOfRows);
271+
return chebyshevEvaluation1D(par[0], coefs1D, mNumberOfRows);
236272
}
237273

238274
/// Evaluates Chebyshev parameterization for 3D function.
239275
/// VERY IMPORTANT: par must contain the function arguments ALREADY MAPPED to [-1:1] interval
240276
inline Double_t Chebyshev3DCalc::Eval(const Double_t* par) const
241277
{
278+
EvalScratch scratch(mNumberOfRows, mNumberOfColumns);
279+
Float_t* coefs2D = scratch.cols();
280+
Float_t* coefs1D = scratch.rows();
242281
for (int id0 = mNumberOfRows; id0--;) {
243282
int nCLoc = mNumberOfColumnsAtRow[id0]; // number of significant coefs on this row
244283
int col0 = mColumnAtRowBeginning[id0]; // beginning of local column in the 2D boundary matrix
245284
for (int id1 = nCLoc; id1--;) {
246285
int id = id1 + col0;
247-
mTemporaryCoefficients2D[id1] = chebyshevEvaluation1D(par[2], mCoefficients + mCoefficientBound2D1[id], mCoefficientBound2D0[id]);
286+
coefs2D[id1] = chebyshevEvaluation1D(par[2], mCoefficients + mCoefficientBound2D1[id], mCoefficientBound2D0[id]);
248287
}
249-
mTemporaryCoefficients1D[id0] = chebyshevEvaluation1D(par[1], mTemporaryCoefficients2D, nCLoc);
288+
coefs1D[id0] = chebyshevEvaluation1D(par[1], coefs2D, nCLoc);
250289
}
251-
return chebyshevEvaluation1D(par[0], mTemporaryCoefficients1D, mNumberOfRows);
290+
return chebyshevEvaluation1D(par[0], coefs1D, mNumberOfRows);
252291
}
253292
} // namespace math_utils
254293
} // namespace o2

0 commit comments

Comments
 (0)