Skip to content

Commit d0703a0

Browse files
sawenzelclaude
andcommitted
Add a multithreaded TPC digitizer device
This adds an optional single TPC digitizer device that digitizes the sectors with several threads instead of lanes. - --tpc-digi-threads N replaces the TPC lanes by one device; it requires --tpc-chunked-writer. - Each thread owns a digitizer with sector seeding, its own hit chains and its own tpc_driftime_digits_lane file, so the existing merger is unchanged. - The CCDB objects are handled once on the device thread, and the distortion maps are recalculated once and shared. - The digits are identical to the lane spec with --tpc-sector-seeding. https://its.cern.ch/jira/browse/O2-7309 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent e18ec93 commit d0703a0

4 files changed

Lines changed: 373 additions & 2 deletions

File tree

‎Steer/DigitizerWorkflow/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ o2_add_executable(digitizer-workflow
2626
src/SimpleDigitizerWorkflow.cxx
2727
src/TPCDigitizerSpec.cxx
2828
src/TPCSectorDigitization.cxx
29+
src/TPCDigitizerMTSpec.cxx
2930
src/ZDCDigitizerSpec.cxx
3031
src/TOFDigitizerSpec.cxx
3132
$<$<BOOL:${ENABLE_UPGRADES}>:src/ITS3DigitizerSpec.cxx>

‎Steer/DigitizerWorkflow/src/SimpleDigitizerWorkflow.cxx‎

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,6 +186,7 @@ void customize(std::vector<o2::framework::ConfigParamSpec>& workflowOptions)
186186
// Option to write TPC digits internaly, without forwarding to a special writer instance.
187187
// This is useful in GRID productions with small available memory.
188188
workflowOptions.push_back(ConfigParamSpec{"tpc-chunked-writer", o2::framework::VariantType::Bool, false, {"Write independent TPC digit chunks as soon as they can be flushed."}});
189+
workflowOptions.push_back(ConfigParamSpec{"tpc-digi-threads", o2::framework::VariantType::Int, 0, {"Digitize TPC in a single device with this many threads instead of lanes (0 = use lanes); requires --tpc-chunked-writer"}});
189190
workflowOptions.push_back(ConfigParamSpec{"tpc-distortion-type", o2::framework::VariantType::Int, 0, {"Simulate distortions in the TPC (0=no distortions, 1=distortions without scaling, 2=distortions with CTP scaling)"}});
190191

191192
std::string simhelp("Comma separated list of simulation prefixes (for background, signal productions)");
@@ -624,8 +625,16 @@ WorkflowSpec defineDataProcessing(ConfigContext const& configcontext)
624625

625626
auto internalwrite = configcontext.options().get<bool>("tpc-chunked-writer");
626627
auto distortionType = configcontext.options().get<int>("tpc-distortion-type");
627-
WorkflowSpec tpcPipelines = o2::tpc::getTPCDigitizerSpec(lanes, tpcsectors, mctruth, internalwrite, distortionType);
628-
specs.insert(specs.end(), tpcPipelines.begin(), tpcPipelines.end());
628+
auto digiThreads = configcontext.options().get<int>("tpc-digi-threads");
629+
if (digiThreads > 0) {
630+
if (!internalwrite) {
631+
LOG(fatal) << "--tpc-digi-threads requires --tpc-chunked-writer";
632+
}
633+
specs.emplace_back(o2::tpc::getTPCDigitizerMTSpec(digiThreads, tpcsectors, distortionType));
634+
} else {
635+
WorkflowSpec tpcPipelines = o2::tpc::getTPCDigitizerSpec(lanes, tpcsectors, mctruth, internalwrite, distortionType);
636+
specs.insert(specs.end(), tpcPipelines.begin(), tpcPipelines.end());
637+
}
629638

630639
if (configcontext.options().get<std::string>("tpc-reco-type").empty() == false) {
631640
throw std::runtime_error("option 'tpc-reco-type' is deprecated, please connect workflows on the command line by pipe");
Lines changed: 358 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,358 @@
1+
// Copyright 2019-2020 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+
/// \file TPCDigitizerMTSpec.cxx
13+
/// \brief A single TPC digitizer device that digitizes the sectors with several threads
14+
15+
#include "TPCDigitizerSpec.h"
16+
#include "TPCSectorDigitization.h"
17+
#include "Framework/ConfigParamRegistry.h"
18+
#include "Framework/DataProcessorSpec.h"
19+
#include "Framework/DataRefUtils.h"
20+
#include "Framework/Lifetime.h"
21+
#include "Framework/CCDBParamSpec.h"
22+
#include "Framework/Task.h"
23+
#include "Framework/Logger.h"
24+
#include "Headers/DataHeader.h"
25+
#include "DataFormatsParameters/GRPObject.h"
26+
#include "DataFormatsTPC/TPCSectorHeader.h"
27+
#include "DetectorsBase/BaseDPLDigitizer.h"
28+
#include "DetectorsBase/Detector.h"
29+
#include "SimConfig/DigiParams.h"
30+
#include "SimulationDataFormat/DigitizationContext.h"
31+
#include "TPCBaseRecSim/CDBInterface.h"
32+
#include "TPCBase/ParameterGEM.h"
33+
#include "TPCBase/ParameterGas.h"
34+
#include "TPCCalibration/VDriftHelper.h"
35+
#include "TPCSimulation/Digitizer.h"
36+
#include "TPCSpaceCharge/SpaceCharge.h"
37+
#include <TChain.h>
38+
#include <TFile.h>
39+
#include <TROOT.h>
40+
#include <TRandom.h>
41+
#include <TStopwatch.h>
42+
#include <TTree.h>
43+
#include <algorithm>
44+
#include <atomic>
45+
#include <exception>
46+
#include <filesystem>
47+
#include <memory>
48+
#include <mutex>
49+
#include <optional>
50+
#include <string>
51+
#include <thread>
52+
#include <utility>
53+
#include <vector>
54+
55+
using namespace o2::framework;
56+
using SC = o2::tpc::SpaceCharge<float>;
57+
58+
namespace o2
59+
{
60+
namespace tpc
61+
{
62+
63+
namespace
64+
{
65+
/// The state of one worker thread: a digitizer, its own hit chains and its own digit file
66+
struct DigitizerWorker {
67+
int id = 0;
68+
Digitizer digitizer;
69+
std::vector<TChain*> chains;
70+
SectorFlushBuffers buffers;
71+
std::exception_ptr error;
72+
73+
~DigitizerWorker()
74+
{
75+
for (auto chain : chains) {
76+
delete chain;
77+
}
78+
}
79+
80+
std::string fileName() const { return "tpc_driftime_digits_lane" + std::to_string(id) + ".root"; }
81+
82+
/// Digitize one sector and write it to the worker's digit file, in the format of the chunked writer of the lane spec
83+
void processSector(const steer::DigitizationContext& context, int sector, ULong64_t timeframe, bool withMCTruth, std::mutex& cdbMutex)
84+
{
85+
std::unique_ptr<TFile> file(TFile::Open(fileName().c_str(), "UPDATE"));
86+
if (!file || file->IsZombie()) {
87+
throw std::runtime_error("TPC: cannot open " + fileName());
88+
}
89+
auto tree = new TTree(std::to_string(sector).c_str(), "o2sim");
90+
tree->SetDirectory(file.get()); // owned by the file
91+
size_t flushCounter = 0;
92+
auto onFlush = [&]() {
93+
++flushCounter;
94+
auto fill = [&tree](const std::string& name, auto* data) {
95+
auto br = o2::base::getOrMakeBranch(*tree, name.c_str(), data);
96+
br->Fill();
97+
br->ResetAddress();
98+
};
99+
fill("TPCDigit_" + std::to_string(sector), &buffers.digits);
100+
if (withMCTruth) {
101+
fill("TPCDigitMCTruth_" + std::to_string(sector), &buffers.labels);
102+
}
103+
fill("TPCCommonMode_" + std::to_string(sector), &buffers.commonMode);
104+
};
105+
digitizeSector(digitizer, context, chains, sector, timeframe, buffers, onFlush, &cdbMutex);
106+
tree->SetEntries(flushCounter);
107+
file->Write("", TObject::kOverwrite);
108+
file->Close();
109+
}
110+
};
111+
} // namespace
112+
113+
class TPCDPLDigitizerMTTask : public o2::base::BaseDPLDigitizer
114+
{
115+
public:
116+
TPCDPLDigitizerMTTask(int nThreads, int distortionType) : BaseDPLDigitizer(o2::base::InitServices::FIELD | o2::base::InitServices::GEOM), mNThreads(nThreads), mDistortionType(distortionType) {}
117+
118+
void initDigitizerTask(framework::InitContext& ic) override
119+
{
120+
LOG(info) << "Initializing multithreaded TPC digitization with " << mNThreads << " threads";
121+
122+
mWithMCTruth = o2::conf::DigiParams::Instance().mctruth;
123+
const auto triggeredMode = ic.options().get<bool>("TPCtriggered");
124+
mRecalcDistortions = !(ic.options().get<bool>("do-not-recalculate-distortions"));
125+
SC::setNThreads(ic.options().get<int>("n-threads-distortions"));
126+
mUseCalibrationsFromCCDB = ic.options().get<bool>("TPCuseCCDB");
127+
mMeanLumiDistortions = ic.options().get<float>("meanLumiDistortions");
128+
mMeanLumiDistortionsDerivative = ic.options().get<float>("meanLumiDistortionsDerivative");
129+
LOG(info) << "TPC calibrations from CCDB: " << mUseCalibrationsFromCCDB;
130+
131+
// the workers share no random numbers: each digitizer seeds every sector from DigiParams.seed
132+
const auto seed = o2::conf::DigiParams::Instance().seed;
133+
if (seed == 0) {
134+
LOG(warning) << "TPC sector seeding with DigiParams.seed=0 is not reproducible";
135+
}
136+
// with seed 0, gRandom was seeded randomly in the base class
137+
const ULong64_t sectorSeed = seed != 0 ? seed : gRandom->Integer(kMaxUInt);
138+
139+
for (int i = 0; i < mNThreads; ++i) {
140+
auto& worker = mWorkers.emplace_back(std::make_unique<DigitizerWorker>());
141+
worker->id = i;
142+
worker->digitizer.setContinuousReadout(!triggeredMode);
143+
worker->digitizer.setDistortionScaleType(mDistortionType);
144+
worker->digitizer.setUseSectorSeeding(sectorSeed);
145+
}
146+
147+
// the merger picks up every lane file in the directory, so files of an earlier run must not survive
148+
for (const auto& entry : std::filesystem::directory_iterator(".")) {
149+
const auto name = entry.path().filename().string();
150+
if (name.rfind("tpc_driftime_digits_lane", 0) == 0 && entry.path().extension() == ".root") {
151+
std::filesystem::remove(entry.path());
152+
}
153+
}
154+
155+
ROOT::EnableThreadSafety();
156+
}
157+
158+
void finaliseCCDB(framework::ConcreteDataMatcher& matcher, void* obj)
159+
{
160+
if (mTPCVDriftHelper.accountCCDBInputs(matcher, obj)) {
161+
return;
162+
}
163+
// the maps are set on the first digitizer; the others share them once they are final
164+
auto& digitizer = mWorkers[0]->digitizer;
165+
if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDIST", 0)) {
166+
LOGP(info, "Updating distortion map");
167+
digitizer.setUseSCDistortions(static_cast<SC*>(obj));
168+
if (mMeanLumiDistortions >= 0) {
169+
digitizer.setMeanLumiDistortions(mMeanLumiDistortions);
170+
}
171+
}
172+
if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCDISTDERIV", 0)) {
173+
LOGP(info, "Updating reference distortion map");
174+
digitizer.setSCDistortionsDerivative(static_cast<SC*>(obj));
175+
if (mMeanLumiDistortionsDerivative >= 0) {
176+
digitizer.setMeanLumiDistortionsDerivative(mMeanLumiDistortionsDerivative);
177+
}
178+
}
179+
if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGASPARAM", 0)) {
180+
LOGP(info, "TPC gas param updated");
181+
ParameterGas::Instance().printKeyValues(true, true);
182+
}
183+
if (matcher == ConcreteDataMatcher(o2::header::gDataOriginTPC, "TPCGEMPARAM", 0)) {
184+
LOGP(info, "TPC GEM param updated");
185+
ParameterGEM::Instance().printKeyValues(true, true);
186+
}
187+
}
188+
189+
void run(framework::ProcessingContext& pc)
190+
{
191+
LOG(info) << "Processing multithreaded TPC digitization";
192+
193+
auto& cdb = o2::tpc::CDBInterface::instance();
194+
cdb.setUseDefaults(!mUseCalibrationsFromCCDB);
195+
// whatever are global settings for CCDB usage, we have to extract the TPC vdrift from CCDB for anchored simulations
196+
mTPCVDriftHelper.extractCCDBInputs(pc);
197+
pc.inputs().get<ParameterGas*>("gasparam");
198+
pc.inputs().get<ParameterGEM*>("gemparam");
199+
auto& primary = mWorkers[0]->digitizer;
200+
if (mDistortionType) {
201+
pc.inputs().get<SC*>("tpcdistortions");
202+
if (mDistortionType == 2) {
203+
pc.inputs().get<SC*>("tpcdistortionsderiv");
204+
primary.setLumiScaleFactor();
205+
if (mRecalcDistortions) {
206+
// done once, with the threads of SpaceCharge, for all workers
207+
primary.recalculateDistortions();
208+
}
209+
}
210+
for (size_t i = 1; i < mWorkers.size(); ++i) {
211+
mWorkers[i]->digitizer.setSCDistortionsFrom(primary);
212+
}
213+
}
214+
215+
if (mTPCVDriftHelper.isUpdated()) {
216+
const auto& vd = mTPCVDriftHelper.getVDriftObject();
217+
LOGP(info, "Updating TPC fast transform map with new VDrift factor of {} wrt reference {} and DriftTimeOffset correction {} wrt {} from source {}",
218+
vd.corrFact, vd.refVDrift, vd.timeOffsetCorr, vd.refTimeOffset, mTPCVDriftHelper.getSourceName());
219+
for (auto& worker : mWorkers) {
220+
worker->digitizer.setVDrift(vd.getVDrift());
221+
worker->digitizer.setTDriftOffset(vd.getTimeOffset());
222+
}
223+
mTPCVDriftHelper.acknowledgeUpdate();
224+
}
225+
226+
if (std::filesystem::exists("ThresholdMap.root")) {
227+
LOG(info) << "TPC: Using zero suppression map from 'ThresholdMap.root'";
228+
cdb.setThresholdMapFromFile("ThresholdMap.root");
229+
}
230+
231+
if (std::filesystem::exists("GainMap.root")) {
232+
LOG(info) << "TPC: Using gain map from 'GainMap.root'";
233+
cdb.setGainMapFromFile("GainMap.root");
234+
}
235+
236+
// the sectors of this timeframe; all inputs carry the same collision context
237+
std::vector<int> sectors;
238+
std::optional<DataRef> contextRef;
239+
for (auto it = pc.inputs().begin(), end = pc.inputs().end(); it != end; ++it) {
240+
for (auto const& inputref : it.parts()) {
241+
if (inputref.spec->lifetime == o2::framework::Lifetime::Condition) {
242+
continue;
243+
}
244+
auto const* sectorHeader = DataRefUtils::getHeader<TPCSectorHeader*>(inputref);
245+
if (sectorHeader == nullptr) {
246+
LOG(error) << "TPC: Sector header missing, skipping input";
247+
continue;
248+
}
249+
const int sector = sectorHeader->sector();
250+
if (sector < 0 || sector >= TPCSectorHeader::NSectors) {
251+
throw std::runtime_error("Digitizer can only work on single sectors");
252+
}
253+
sectors.push_back(sector);
254+
if (!contextRef) {
255+
contextRef = inputref;
256+
}
257+
}
258+
}
259+
260+
if (mWriteGRP) {
261+
auto roMode = primary.isContinuousReadout() ? o2::parameters::GRPObject::CONTINUOUS : o2::parameters::GRPObject::PRESENT;
262+
LOG(info) << "TPC: Sending ROMode= " << (primary.isContinuousReadout() ? "Continuous" : "Triggered") << " to GRPUpdater";
263+
pc.outputs().snapshot(Output{"TPC", "ROMode", 0}, roMode);
264+
mWriteGRP = false;
265+
}
266+
if (!contextRef) {
267+
return;
268+
}
269+
auto context = pc.inputs().get<o2::steer::DigitizationContext*>(*contextRef);
270+
const ULong64_t timeframe = DataRefUtils::getHeader<o2::header::DataHeader*>(*contextRef)->tfCounter;
271+
LOG(info) << "TPC: Processing " << sectors.size() << " sectors with " << context->getEventRecords().size() << " collisions";
272+
273+
TStopwatch timer;
274+
timer.Start();
275+
276+
const size_t nWorkers = std::min(mWorkers.size(), sectors.size());
277+
for (size_t i = 0; i < nWorkers; ++i) {
278+
context->initSimChains(o2::detectors::DetID::TPC, mWorkers[i]->chains);
279+
}
280+
281+
std::atomic<size_t> nextSector{0};
282+
// serialises CDBInterface access; the CCDB timestamp must not change while the workers run
283+
std::mutex cdbMutex;
284+
std::vector<std::thread> threads;
285+
for (size_t i = 0; i < nWorkers; ++i) {
286+
threads.emplace_back([&, worker = mWorkers[i].get()]() {
287+
try {
288+
for (size_t is = nextSector++; is < sectors.size(); is = nextSector++) {
289+
LOG(info) << "TPC: Worker " << worker->id << " processing sector " << sectors[is];
290+
worker->processSector(*context, sectors[is], timeframe, mWithMCTruth, cdbMutex);
291+
}
292+
} catch (...) {
293+
worker->error = std::current_exception();
294+
nextSector = sectors.size(); // let the other workers stop early
295+
}
296+
});
297+
}
298+
for (auto& thread : threads) {
299+
thread.join();
300+
}
301+
for (size_t i = 0; i < nWorkers; ++i) {
302+
if (mWorkers[i]->error) {
303+
std::rethrow_exception(mWorkers[i]->error);
304+
}
305+
}
306+
307+
timer.Stop();
308+
LOG(info) << "TPC: Digitization took " << timer.CpuTime() << "s CPU, " << timer.RealTime() << "s wall";
309+
}
310+
311+
private:
312+
std::vector<std::unique_ptr<DigitizerWorker>> mWorkers;
313+
o2::tpc::VDriftHelper mTPCVDriftHelper{};
314+
int mNThreads = 1;
315+
int mDistortionType = 0;
316+
bool mWriteGRP = true;
317+
bool mWithMCTruth = true;
318+
bool mUseCalibrationsFromCCDB = false;
319+
bool mRecalcDistortions = false;
320+
float mMeanLumiDistortions = -1;
321+
float mMeanLumiDistortionsDerivative = -1;
322+
};
323+
324+
o2::framework::DataProcessorSpec getTPCDigitizerMTSpec(int nThreads, std::vector<int> const& sectors, int distortionType)
325+
{
326+
std::vector<InputSpec> inputs;
327+
for (auto sector : sectors) {
328+
inputs.emplace_back("collisioncontext" + std::to_string(sector), "SIM", "COLLISIONCONTEXT", static_cast<o2::header::DataHeader::SubSpecificationType>(sector), Lifetime::Timeframe);
329+
}
330+
o2::tpc::VDriftHelper::requestCCDBInputs(inputs);
331+
inputs.emplace_back("gasparam", o2::header::gDataOriginTPC, "TPCGASPARAM", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::ParGas)));
332+
inputs.emplace_back("gemparam", o2::header::gDataOriginTPC, "TPCGEMPARAM", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::ParGEM)));
333+
if (distortionType) {
334+
inputs.emplace_back("tpcdistortions", o2::header::gDataOriginTPC, "TPCDIST", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::DistortionMapMC), {}, 1)); // time-dependent
335+
if (distortionType == 2) {
336+
inputs.emplace_back("tpcdistortionsderiv", o2::header::gDataOriginTPC, "TPCDISTDERIV", 0, Lifetime::Condition, ccdbParamSpec(o2::tpc::CDBTypeMap.at(o2::tpc::CDBType::DistortionMapDerivMC), {}, 1)); // time-dependent
337+
}
338+
}
339+
std::vector<OutputSpec> outputs{{"TPC", "ROMode", 0, Lifetime::Timeframe}};
340+
341+
// the name must not match "TPCDigitizer.*", whose completion policy would process the sectors one by one
342+
return DataProcessorSpec{
343+
"tpc-digitizer-mt",
344+
inputs,
345+
outputs,
346+
AlgorithmSpec{adaptFromTask<TPCDPLDigitizerMTTask>(nThreads, distortionType)},
347+
Options{
348+
{"TPCtriggered", VariantType::Bool, false, {"Impose triggered RO mode (default: continuous)"}},
349+
{"TPCuseCCDB", VariantType::Bool, false, {"true: load calibrations from CCDB; false: use random calibratoins"}},
350+
{"meanLumiDistortions", VariantType::Float, -1.f, {"override lumi of distortion object if >=0"}},
351+
{"meanLumiDistortionsDerivative", VariantType::Float, -1.f, {"override lumi of derivative distortion object if >=0"}},
352+
{"do-not-recalculate-distortions", VariantType::Bool, false, {"Do not recalculate the distortions"}},
353+
{"n-threads-distortions", VariantType::Int, 4, {"Number of threads used for the calculation of the distortions"}},
354+
}};
355+
}
356+
357+
} // namespace tpc
358+
} // namespace o2

0 commit comments

Comments
 (0)