Skip to content

Commit abafa83

Browse files
[Infrastructure,Tools,Common,Tutorial,PWGDQ,PWGHF,PWGLF] [ML] Fix for memory issue in ML headers (#17836)
Co-authored-by: ALICE Action Bot <alibuild@cern.ch>
1 parent db8aaf5 commit abafa83

11 files changed

Lines changed: 296 additions & 95 deletions

File tree

‎CODEOWNERS‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@
7878
/PWGUD @alibuild @amatyja @rolavick
7979
/PWGJE @alibuild @nzardosh @fjonasALICE @jaimenorman @mhemmer-cern
8080
/Tools/PIDML @alibuild @saganatt
81-
/Tools/ML @alibuild @fcatalan92 @fmazzasc
81+
/Tools/ML @alibuild @fcatalan92 @fmazzasc @ChSonnabend
8282
/Tutorials/PWGCF @alibuild @jgrosseo @victor-gonzalez @zchochul
8383
/Tutorials/PWGDQ @alibuild @iarsene @mcoquet642 @XiaozhiBai @mguilbau
8484
/Tutorials/PWGEM @alibuild @mikesas @rbailhac @dsekihat @ivorobye @feisenhu

‎Common/Tools/PID/pidTPCModule.h‎

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
#include <TRandom.h>
5050
#include <TString.h>
5151

52+
#include <algorithm>
5253
#include <array>
5354
#include <chrono>
5455
#include <cstddef>
@@ -570,11 +571,12 @@ class pidTPCModule
570571
std::unique_ptr<float[]> networkPrediction(new float[predictionSize * NParticleTypes]); // For each mass hypotheses
571572

572573
const float nNclNormalization = response->GetNClNormalization();
573-
float durationNetwork = 0;
574+
float durationNetwork = 0.f;
574575

575576
std::vector<float> trackProperties(trackPropSize);
577+
std::vector<float> outputNetwork; // output buffer, allocation is reused for all mass hypotheses
576578
uint64_t counterTrackProps = 0;
577-
int loopCounter = 0;
579+
uint64_t loopCounter = 0;
578580

579581
// To load the Hadronic rate once for each collision
580582
std::vector<float> hadronicRateForCollision(collisions.size(), 0.0f);
@@ -626,14 +628,13 @@ class pidTPCModule
626628
}
627629

628630
const auto startNetworkEval = std::chrono::high_resolution_clock::now();
629-
const float* const outputNetwork = network.evalModel(trackProperties);
631+
network.evalModel(trackProperties, outputNetwork);
630632
const auto stopNetworkEval = std::chrono::high_resolution_clock::now();
631633
durationNetwork += std::chrono::duration<float, std::ratio<1, NanoToOne>>(stopNetworkEval - startNetworkEval).count();
632-
for (uint64_t kPrediction = 0; kPrediction < predictionSize; kPrediction += outputDimensions) {
633-
for (int lOutputDim = 0; lOutputDim < outputDimensions; ++lOutputDim) {
634-
networkPrediction[kPrediction + lOutputDim + predictionSize * loopCounter] = outputNetwork[kPrediction + lOutputDim];
635-
}
634+
if (outputNetwork.size() != predictionSize) {
635+
LOG(fatal) << "Network output size (" << outputNetwork.size() << ") does not match the expected prediction size (" << predictionSize << ")";
636636
}
637+
std::copy(outputNetwork.begin(), outputNetwork.end(), networkPrediction.get() + predictionSize * loopCounter);
637638

638639
counterTrackProps = 0;
639640
++loopCounter;

‎PWGDQ/Tasks/quarkoniaToHyperons.cxx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1773,7 +1773,7 @@ struct QuarkoniaToHyperons {
17731773
float k0shortScore = -1;
17741774
if (mlConfigurations.calculateK0ShortScores) {
17751775
// evaluate machine-learning scores
1776-
float* k0shortProbability = mlCustomModelK0Short.evalModel(inputFeatures);
1776+
const std::vector<float> k0shortProbability = mlCustomModelK0Short.evalModel(inputFeatures);
17771777
k0shortScore = k0shortProbability[1];
17781778
} else {
17791779
k0shortScore = v0.k0ShortBDTScore();
@@ -1788,7 +1788,7 @@ struct QuarkoniaToHyperons {
17881788
float lambdaScore = -1;
17891789
if (mlConfigurations.calculateLambdaScores) {
17901790
// evaluate machine-learning scores
1791-
float* lambdaProbability = mlCustomModelLambda.evalModel(inputFeatures);
1791+
const std::vector<float> lambdaProbability = mlCustomModelLambda.evalModel(inputFeatures);
17921792
lambdaScore = lambdaProbability[1];
17931793
} else {
17941794
lambdaScore = v0.lambdaBDTScore();
@@ -1803,7 +1803,7 @@ struct QuarkoniaToHyperons {
18031803
float antiLambdaScore = -1;
18041804
if (mlConfigurations.calculateAntiLambdaScores) {
18051805
// evaluate machine-learning scores
1806-
float* antilambdaProbability = mlCustomModelAntiLambda.evalModel(inputFeatures);
1806+
const std::vector<float> antilambdaProbability = mlCustomModelAntiLambda.evalModel(inputFeatures);
18071807
antiLambdaScore = antilambdaProbability[1];
18081808
} else {
18091809
antiLambdaScore = v0.antiLambdaBDTScore();

‎PWGHF/TableProducer/candidateSelectorLcPidMl.cxx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -307,12 +307,12 @@ struct HfCandidateSelectorLcPidMl {
307307
std::vector<double> inputFeaturesD{trackParPos1.getPt(), trackPos1.dcaXY(), trackPos1.dcaZ(), trackParNeg.getPt(), trackNeg.dcaXY(), trackNeg.dcaZ(), trackParPos2.getPt(), trackPos2.dcaXY(), trackPos2.dcaZ()};
308308
float scores[3] = {-1.f, -1.f, -1.f};
309309
if (dataTypeML == 1) {
310-
auto* scoresRaw = model.evalModel(inputFeaturesF);
310+
const auto scoresRaw = model.evalModel(inputFeaturesF);
311311
for (int iScore = 0; iScore < 3; ++iScore) {
312312
scores[iScore] = scoresRaw[iScore];
313313
}
314314
} else if (dataTypeML == 11) {
315-
auto* scoresRaw = model.evalModel(inputFeaturesD);
315+
const auto scoresRaw = model.evalModel(inputFeaturesD);
316316
for (int iScore = 0; iScore < 3; ++iScore) {
317317
scores[iScore] = scoresRaw[iScore];
318318
}

‎PWGLF/TableProducer/Strangeness/lambdakzeromlselection.cxx‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,19 +210,19 @@ struct lambdakzeromlselection {
210210

211211
// calculate classifier output
212212
if (PredictLambda) {
213-
float* LambdaProbability = lambda_bdt.evalModel(inputFeatures);
213+
const std::vector<float> LambdaProbability = lambda_bdt.evalModel(inputFeatures);
214214
lambdaMLSelections(LambdaProbability[1]);
215215
}
216216
if (PredictGamma) {
217-
float* GammaProbability = gamma_bdt.evalModel(inputFeatures);
217+
const std::vector<float> GammaProbability = gamma_bdt.evalModel(inputFeatures);
218218
gammaMLSelections(GammaProbability[1]);
219219
}
220220
if (PredictAntiLambda) {
221-
float* AntiLambdaProbability = antilambda_bdt.evalModel(inputFeatures);
221+
const std::vector<float> AntiLambdaProbability = antilambda_bdt.evalModel(inputFeatures);
222222
antiLambdaMLSelections(AntiLambdaProbability[1]);
223223
}
224224
if (PredictKZeroShort) {
225-
float* KZeroShortProbability = kzeroshort_bdt.evalModel(inputFeatures);
225+
const std::vector<float> KZeroShortProbability = kzeroshort_bdt.evalModel(inputFeatures);
226226
kzeroShortMLSelections(KZeroShortProbability[1]);
227227
}
228228
}

‎PWGLF/TableProducer/Strangeness/strangenessbuilder.cxx‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1006,7 +1006,7 @@ struct StrangenessBuilder {
10061006
AvgPA, // 6. Avg Pointing Angle
10071007
static_cast<float>(v0zRanks[ic])}; // 7. V0 Vtx z Rank
10081008

1009-
float* BDTProbability = deduplication_bdt.evalModel(inputFeatures);
1009+
const std::vector<float> BDTProbability = deduplication_bdt.evalModel(inputFeatures);
10101010

10111011
if (BDTProbability[1] > bestMLScore) {
10121012
bestMLScore = BDTProbability[1];

‎PWGLF/Tasks/Strangeness/derivedlambdakzeroanalysis.cxx‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1893,7 +1893,7 @@ struct derivedlambdakzeroanalysis {
18931893
float k0shortScore = -1;
18941894
if (mlConfigurations.calculateK0ShortScores) {
18951895
// evaluate machine-learning scores
1896-
float* k0shortProbability = mlCustomModelK0Short.evalModel(inputFeatures);
1896+
const std::vector<float> k0shortProbability = mlCustomModelK0Short.evalModel(inputFeatures);
18971897
k0shortScore = k0shortProbability[1];
18981898
} else {
18991899
k0shortScore = v0.k0ShortBDTScore();
@@ -1908,7 +1908,7 @@ struct derivedlambdakzeroanalysis {
19081908
float lambdaScore = -1;
19091909
if (mlConfigurations.calculateLambdaScores) {
19101910
// evaluate machine-learning scores
1911-
float* lambdaProbability = mlCustomModelLambda.evalModel(inputFeatures);
1911+
const std::vector<float> lambdaProbability = mlCustomModelLambda.evalModel(inputFeatures);
19121912
lambdaScore = lambdaProbability[1];
19131913
} else {
19141914
lambdaScore = v0.lambdaBDTScore();
@@ -1923,7 +1923,7 @@ struct derivedlambdakzeroanalysis {
19231923
float antiLambdaScore = -1;
19241924
if (mlConfigurations.calculateAntiLambdaScores) {
19251925
// evaluate machine-learning scores
1926-
float* antilambdaProbability = mlCustomModelAntiLambda.evalModel(inputFeatures);
1926+
const std::vector<float> antilambdaProbability = mlCustomModelAntiLambda.evalModel(inputFeatures);
19271927
antiLambdaScore = antilambdaProbability[1];
19281928
} else {
19291929
antiLambdaScore = v0.antiLambdaBDTScore();

‎Tools/ML/MlResponse.h‎

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -190,8 +190,16 @@ class MlResponse
190190
LOG(fatal) << "Number of input nodes in the model " << mPaths[nModel] << " is different from the number of input features to be tested (" << numInputNodes << " vs " << numInputFeatures << ")";
191191
}
192192

193-
TypeOutputScore* outputPtr = mModels[nModel].template evalModel<TypeOutputScore>(input);
194-
return std::vector<TypeOutputScore>{outputPtr, outputPtr + mNClasses};
193+
// evalModel returns an owning copy of the (last) output tensor of the model
194+
std::vector<TypeOutputScore> output = mModels[nModel].template evalModel<TypeOutputScore>(input);
195+
if (output.size() < mNClasses) {
196+
LOG(fatal) << "Model " << mPaths[nModel] << " returned " << output.size() << " scores, but " << static_cast<int>(mNClasses) << " classes are expected. Please check your configurables.";
197+
}
198+
if (output.size() > mNClasses) {
199+
// keep only the first mNClasses scores (e.g. single-candidate probabilities of a multi-output model)
200+
output.resize(mNClasses);
201+
}
202+
return output;
195203
}
196204

197205
/// Get vector with model predictions for a batch of candidates
@@ -221,11 +229,16 @@ class MlResponse
221229
LOG(fatal) << "Number of input nodes in the model " << mPaths[nModel] << " differs from features per row (" << numInputNodes << " vs " << featuresPerRow << ")";
222230
}
223231

224-
TypeOutputScore* outputPtr = mModels[nModel].template evalModel<TypeOutputScore>(input);
225-
if (outputPtr == nullptr) {
226-
LOG(fatal) << "Batched model evaluation failed for model " << mPaths[nModel];
232+
std::vector<TypeOutputScore> output = mModels[nModel].template evalModel<TypeOutputScore>(input);
233+
const std::size_t expectedOutputSize = nRows * mNClasses;
234+
if (output.size() < expectedOutputSize) {
235+
LOG(fatal) << "Model " << mPaths[nModel] << " returned " << output.size() << " scores, but " << expectedOutputSize << " scores are expected for " << nRows << " rows and " << static_cast<int>(mNClasses) << " classes. Please check your configurables.";
236+
}
237+
if (output.size() > expectedOutputSize) {
238+
// keep only the first scores (e.g. batched probabilities of a multi-output model)
239+
output.resize(expectedOutputSize);
227240
}
228-
return std::vector<TypeOutputScore>{outputPtr, outputPtr + nRows * mNClasses};
241+
return output;
229242
}
230243

231244
/// ML selections

‎Tools/ML/model.cxx‎

Lines changed: 126 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,9 @@ namespace ml
4242

4343
std::string OnnxModel::printShape(const std::vector<int64_t>& v)
4444
{
45+
if (v.empty()) {
46+
return "[]";
47+
}
4548
std::stringstream ss("");
4649
for (std::size_t i = 0; i < v.size() - 1; i++)
4750
ss << v[i] << "x";
@@ -90,7 +93,14 @@ void OnnxModel::initModel(const std::string& localPath, const bool enableOptimiz
9093

9194
mEnv = std::make_shared<Ort::Env>(ORT_LOGGING_LEVEL_WARNING, "onnx-model");
9295
mSession = std::make_shared<Ort::Session>(*mEnv, modelPath.c_str(), sessionOptions);
96+
mMemInfo = Ort::MemoryInfo::CreateCpu(OrtAllocatorType::OrtArenaAllocator, OrtMemType::OrtMemTypeDefault);
9397

98+
mInputNamesChar.clear();
99+
mOutputNamesChar.clear();
100+
mInputNames.clear();
101+
mInputShapes.clear();
102+
mOutputNames.clear();
103+
mOutputShapes.clear();
94104
Ort::AllocatorWithDefaultOptions const tmpAllocator;
95105
for (std::size_t i = 0; i < mSession->GetInputCount(); ++i) {
96106
mInputNames.push_back(mSession->GetInputNameAllocated(i, tmpAllocator).get());
@@ -104,6 +114,14 @@ void OnnxModel::initModel(const std::string& localPath, const bool enableOptimiz
104114
for (std::size_t i = 0; i < mSession->GetOutputCount(); ++i) {
105115
mOutputShapes.emplace_back(mSession->GetOutputTypeInfo(i).GetTensorTypeAndShapeInfo().GetShape());
106116
}
117+
mInputNamesChar.reserve(mInputNames.size());
118+
for (const auto& name : mInputNames) {
119+
mInputNamesChar.push_back(name.c_str());
120+
}
121+
mOutputNamesChar.reserve(mOutputNames.size());
122+
for (const auto& name : mOutputNames) {
123+
mOutputNamesChar.push_back(name.c_str());
124+
}
107125
LOG(info) << "Input Nodes:";
108126
for (std::size_t i = 0; i < mInputNames.size(); i++) {
109127
LOG(info) << "\t" << mInputNames[i] << " : " << printShape(mInputShapes[i]);
@@ -122,6 +140,114 @@ void OnnxModel::initModel(const std::string& localPath, const bool enableOptimiz
122140
LOG(info) << "--- Model initialized! ---";
123141
}
124142

143+
std::vector<int64_t> OnnxModel::inferInputShape(const std::size_t iinput, const int64_t size) const
144+
{
145+
const std::vector<int64_t>& modelShape = mInputShapes[iinput];
146+
147+
// Rank-1 input: the whole vector is the tensor
148+
if (modelShape.size() < 2) {
149+
return {size};
150+
}
151+
152+
// Product of all non-batch dimensions; dynamic dimensions (< 0) cannot be inferred
153+
int64_t totalSize = 1;
154+
bool hasDynamicDim = false;
155+
for (std::size_t idim = 1; idim < modelShape.size(); idim++) {
156+
if (modelShape[idim] < 0) {
157+
hasDynamicDim = true;
158+
} else {
159+
totalSize *= modelShape[idim];
160+
}
161+
}
162+
163+
if (hasDynamicDim) {
164+
if (modelShape.size() == 2) {
165+
// [batch, features] with dynamic feature dimension: interpret the vector as a single sample
166+
return {1, size};
167+
}
168+
LOG(fatal) << "Input " << iinput << " (" << mInputNames[iinput] << ") has dynamic non-batch dimensions (" << printShape(modelShape) << "), the tensor shape cannot be inferred from a flat vector. Please provide std::vector<Ort::Value> inputs instead.";
169+
}
170+
171+
if (totalSize <= 0 || size % totalSize != 0) {
172+
LOG(fatal) << "Size of the input vector (" << size << ") is not a multiple of the model input size (" << totalSize << ") for input " << iinput << " (" << mInputNames[iinput] << ", shape " << printShape(modelShape) << ")";
173+
}
174+
175+
std::vector<int64_t> inputShape;
176+
inputShape.reserve(modelShape.size());
177+
inputShape.push_back(size / totalSize);
178+
for (std::size_t idim = 1; idim < modelShape.size(); idim++) {
179+
inputShape.push_back(modelShape[idim]);
180+
}
181+
return inputShape;
182+
}
183+
184+
void OnnxModel::checkInput(const std::vector<Ort::Value>& input) const
185+
{
186+
if (!mSession) {
187+
LOG(fatal) << "OnnxModel::evalModel called before initModel()";
188+
}
189+
if (input.size() != mInputNames.size()) {
190+
LOG(fatal) << "Number of input tensors (" << input.size() << ") does not agree with the number of model inputs (" << mInputNames.size() << ")";
191+
}
192+
for (std::size_t i = 0; i < input.size(); i++) {
193+
LOG(debug) << "Input tensor " << i << " shape: " << printShape(input[i].GetTensorTypeAndShapeInfo().GetShape());
194+
}
195+
}
196+
197+
std::vector<Ort::Value> OnnxModel::evalModelRaw(std::vector<Ort::Value>& input)
198+
{
199+
checkInput(input);
200+
std::vector<Ort::Value> outputTensors;
201+
try {
202+
const Ort::RunOptions runOptions{nullptr};
203+
outputTensors = mSession->Run(runOptions, mInputNamesChar.data(), input.data(), input.size(), mOutputNamesChar.data(), mOutputNamesChar.size());
204+
} catch (const Ort::Exception& exception) {
205+
LOG(fatal) << "Error running model inference: " << exception.what();
206+
}
207+
208+
LOG(debug) << "Number of output tensors: " << outputTensors.size();
209+
if (outputTensors.size() != mOutputNames.size()) {
210+
LOG(fatal) << "Number of output tensors: " << outputTensors.size() << " does not agree with the model specified size: " << mOutputNames.size();
211+
}
212+
for (std::size_t i = 0; i < outputTensors.size(); i++) {
213+
checkOutput(outputTensors[i], i);
214+
}
215+
216+
return outputTensors;
217+
}
218+
219+
Ort::Value OnnxModel::evalModelLast(std::vector<Ort::Value>& input)
220+
{
221+
checkInput(input);
222+
if (mOutputNamesChar.empty()) {
223+
LOG(fatal) << "Model has no outputs";
224+
}
225+
Ort::Value output{nullptr};
226+
try {
227+
// A null RunOptions uses the runtime defaults without allocating options per call.
228+
const Ort::RunOptions runOptions{nullptr};
229+
mSession->Run(runOptions, mInputNamesChar.data(), input.data(), input.size(), &mOutputNamesChar.back(), &output, 1);
230+
} catch (const Ort::Exception& exception) {
231+
LOG(fatal) << "Error running model inference: " << exception.what();
232+
}
233+
checkOutput(output, mOutputShapes.size() - 1);
234+
return output;
235+
}
236+
237+
void OnnxModel::checkOutput(const Ort::Value& tensor, const std::size_t index) const
238+
{
239+
const std::vector<int64_t> shape = tensor.GetTensorTypeAndShapeInfo().GetShape();
240+
LOG(debug) << "Output tensor " << index << " shape: " << printShape(shape);
241+
bool shapeOk = (shape.size() == mOutputShapes[index].size());
242+
for (std::size_t idim = 0; shapeOk && idim < shape.size(); idim++) {
243+
// Dynamic dimensions of the model (< 0) can take any value.
244+
shapeOk = (mOutputShapes[index][idim] < 0) || (shape[idim] == mOutputShapes[index][idim]);
245+
}
246+
if (!shapeOk) {
247+
LOG(fatal) << "Shape of output tensor " << index << " does not agree with model specification! Output: " << printShape(shape) << " model: " << printShape(mOutputShapes[index]);
248+
}
249+
}
250+
125251
void OnnxModel::setActiveThreads(const int threads)
126252
{
127253
activeThreads = threads;

0 commit comments

Comments
 (0)