Rivet API documentation

Rivet 4.1.3
RivetONNXrt.hh
1// -*- C++ -*-
2#ifndef RIVET_RivetONNXrt_HH
3#define RIVET_RivetONNXrt_HH
4
5#include <algorithm>
6#include <functional>
7#include <iostream>
8#include <map>
9#include <numeric>
10
11#include "Rivet/Tools/RivetPaths.hh"
12#include "Rivet/Tools/Utils.hh"
13#include "onnxruntime/onnxruntime_cxx_api.h"
14
15namespace Rivet {
16
17
23 class RivetONNXrt {
24 public:
25
26 // Suppress default constructor
27 RivetONNXrt() = delete;
28
30 RivetONNXrt(const string& filename, const string& runname = "RivetONNXrt", const int maxOrtThreads = 1) {
31
32 // Set some ORT variables that need to be kept in memory
33 _runName = runname;
34 _env = std::make_unique<Ort::Env>(ORT_LOGGING_LEVEL_WARNING, runname.c_str());
35
36 // Set up the ORT session options.
37 Ort::SessionOptions sessionopts;
38
39 // Has the user set RIVET_ORT_MAX_THREADS?
40 const int envMaxOrtThreads = getEnvParam<int>("RIVET_ORT_MAX_THREADS", 0);
41
42 // Set the max number of threads for ONNX RT to use. Beware, ORT is greedy!
43 if (envMaxOrtThreads == 0) {
44 // Default case: set to min(OMP_NUM_THREADS, analysis specified max)
45 const int max_threads = getEnvParam<int>("OMP_NUM_THREADS", maxOrtThreads);
46 const int used_threads = min(maxOrtThreads, max_threads);
47 sessionopts.SetIntraOpNumThreads(used_threads);
48 MSG_DEBUG("Using " << used_threads << " ORT threads");
49 }
50 // if env var specifies a max, take the min of this and the analysis specified
51 else if (envMaxOrtThreads >= 1) {
52 const int used_threads = min(maxOrtThreads, envMaxOrtThreads);
53 MSG_DEBUG("Using " << used_threads << " ORT threads.");
54 sessionopts.SetIntraOpNumThreads(used_threads);
55 }
56 // special debug mode: if envMaxOrtThreads is negative, force overwrite analysis default.
57 else {
58 MSG_WARNING("Using " << abs(envMaxOrtThreads)
59 << " ORT threads, as forced by the negative RIVET_ORT_MAX_THREADS value.");
60 sessionopts.SetIntraOpNumThreads(abs(envMaxOrtThreads));
61 }
62
63
64 // Load the model
65 try {
66 _session = std::make_unique<Ort::Session>(*_env, filename.c_str(), sessionopts);
67 }
68 catch (const std::exception& e) {
69 MSG_ERROR("Failure loading onnx file: " << e.what());
70 }
71
72 // Store network hyperparameters (input/output shape, etc.)
73 getNetworkInfo();
74
75 MSG_DEBUG(*this);
76 }
77
78
82 template <typename T = float>
83 vector<vector<T>> compute(const vector<vector<T>>& inputs) const {
84
85 // Check that number of input nodes matches what the model expects
86 if (inputs.size() != _inDims.size()) {
87 throw DataError("Expected " + to_string(_inDims.size()) + " input nodes, " + "received "
88 + to_string(inputs.size()));
89 }
90
91 // Reject models with non-tensor outputs before running inference
92 for (size_t i = 0; i < _outTypes.size(); ++i) {
93 if (_outTypes[i] == ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED)
94 throw DataError("Output node " + to_string(i) + " (" + string(_outNames[i])
95 + ") is not a tensor — use computeMaps() for Seq(Map) outputs");
96 }
97
98 // Create input tensor objects from input data
99 vector<Ort::Value> ort_input;
100 ort_input.reserve(_inDims.size());
101 auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
102 for (size_t i = 0; i < _inDims.size(); ++i) {
103
104 // Check that input data matches expected input node dimension
105 if (inputs[i].size() != (size_t)_inDimsFlat[i]) {
106 throw DataError("Expected flattened dimension " + to_string(_inDimsFlat[i]) + " for input node "
107 + to_string(i) + ", received " + to_string(inputs[i].size()));
108 }
109
110 // Check that input data matches expected input node type
111 _checkTypes(inputs[i].data(), i); //< bit hacky, but minimises duplication
112
113 ort_input.emplace_back(Ort::Value::CreateTensor<T>(memory_info, const_cast<T*>(inputs[i].data()),
114 inputs[i].size(), _inDims[i].data(),
115 _inDims[i].size()));
116 }
117
118 // Retrieve output tensors
119 auto ort_output = _session->Run(Ort::RunOptions{nullptr}, _inNames.data(), ort_input.data(),
120 ort_input.size(), _outNames.data(), _outNames.size());
121
122 // Construct flattened values and return
123 vector<vector<T>> outputs;
124 outputs.resize(_outDims.size());
125 for (size_t i = 0; i < _outDims.size(); ++i) {
126 T* floatarr = ort_output[i].GetTensorMutableData<T>();
127 outputs[i].assign(floatarr, floatarr + _outDimsFlat[i]);
128 }
129 return outputs;
130 }
131
132
134 template <typename T = float>
135 vector<T> compute(const vector<T>& inputs) const {
136 if (_inDims.size() != 1 || _outDims.size() != 1) {
137 throw("This method assumes a single input/output node!");
138 }
139 vector<vector<T>> wrapped_inputs = {inputs};
140 vector<vector<T>> outputs = compute(wrapped_inputs);
141 return outputs[0];
142 }
143
144
147 template <typename K = long, typename V = float>
148 vector<map<K, V>> computeMaps(const vector<vector<V>>& inputs) const {
149
150 if (inputs.size() != _inDims.size()) {
151 throw DataError("Expected " + to_string(_inDims.size()) + " input nodes, " + "received "
152 + to_string(inputs.size()));
153 }
154
155 // Reject models where all outputs are tensors
156 const bool has_seq_map = std::any_of(_outTypes.begin(), _outTypes.end(), [](auto t) {
157 return t == ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED;
158 });
159 if (!has_seq_map) throw DataError("No Seq(Map) outputs found in this model — use compute() instead");
160
161 vector<Ort::Value> ort_input;
162 ort_input.reserve(_inDims.size());
163 auto memory_info = Ort::MemoryInfo::CreateCpu(OrtArenaAllocator, OrtMemTypeDefault);
164 for (size_t i = 0; i < _inDims.size(); ++i) {
165 if (inputs[i].size() != (size_t)_inDimsFlat[i]) {
166 throw DataError("Expected flattened dimension " + to_string(_inDimsFlat[i]) + " for input node "
167 + to_string(i) + ", received " + to_string(inputs[i].size()));
168 }
169 _checkTypes(inputs[i].data(), i);
170 ort_input.emplace_back(Ort::Value::CreateTensor<V>(memory_info, const_cast<V*>(inputs[i].data()),
171 inputs[i].size(), _inDims[i].data(),
172 _inDims[i].size()));
173 }
174
175 auto ort_output = _session->Run(Ort::RunOptions{nullptr}, _inNames.data(), ort_input.data(),
176 ort_input.size(), _outNames.data(), _outNames.size());
177
178 vector<map<K, V>> outputs(_outDims.size());
179 Ort::AllocatorWithDefaultOptions alloc;
180 for (size_t i = 0; i < _outDims.size(); ++i) {
181 if (ort_output[i].IsTensor()) continue; // tensor outputs return empty map
182 // Unpack Seq(Map(K,V)): extract the map from the first batch element
183 auto map_val = ort_output[i].GetValue(0, alloc);
184 auto keys_val = map_val.GetValue(0, alloc);
185 auto vals_val = map_val.GetValue(1, alloc);
186 const int64_t n = keys_val.GetTensorTypeAndShapeInfo().GetShape()[0];
187 const K* keys = keys_val.GetTensorMutableData<K>();
188 const float* vals = vals_val.GetTensorMutableData<float>();
189 for (int64_t j = 0; j < n; ++j) outputs[i][keys[j]] = static_cast<V>(vals[j]);
190 }
191 return outputs;
192 }
193
194
196 template <typename K = long, typename V = float>
197 map<K, V> computeMap(const vector<V>& inputs) const {
198 if (_inDims.size() != 1 || _outDims.size() != 1) {
199 throw("This method assumes a single input/output node!");
200 }
201 return computeMaps<K, V>({inputs})[0];
202 }
203
204
206 bool hasKey(const std::string& key) const {
207 Ort::AllocatorWithDefaultOptions allocator;
208 return (bool)_metadata->LookupCustomMetadataMapAllocated(key.c_str(), allocator);
209 }
210
211
214 template <typename T, typename std::enable_if_t<!is_iterable_v<T> | is_cstring_v<T>>>
215 T retrieve(const std::string& key) const {
216 Ort::AllocatorWithDefaultOptions allocator;
217 Ort::AllocatedStringPtr res = _metadata->LookupCustomMetadataMapAllocated(key.c_str(), allocator);
218 if (!res) {
219 throw("Key '" + key + "' not found in network metadata!");
220 }
221 /*if constexpr (std::is_same<T, std::string>::value) {
222 return res.get();
223 }*/
224 return lexical_cast<T>(res.get());
225 }
226
228 std::string retrieve(const std::string& key) const {
229 Ort::AllocatorWithDefaultOptions allocator;
230 Ort::AllocatedStringPtr res = _metadata->LookupCustomMetadataMapAllocated(key.c_str(), allocator);
231 if (!res) {
232 throw("Key '" + key + "' not found in network metadata!");
233 }
234 return res.get();
235 }
236
238 template <typename T>
239 vector<T> retrieve(const std::string& key) const {
240 const vector<string> stringvec = split(retrieve(key), ",");
241 vector<T> returnvec = {};
242 for (const string& s : stringvec) {
243 returnvec.push_back(lexical_cast<T>(s));
244 }
245 return returnvec;
246 }
247
249 template <typename T>
250 vector<T> retrieve(const std::string& key, const vector<T>& defaultreturn) const {
251 try {
252 return retrieve<T>(key);
253 }
254 catch (...) {
255 return defaultreturn;
256 }
257 }
258
259 std::string retrieve(const std::string& key, const std::string& defaultreturn) const {
260 try {
261 return retrieve(key);
262 }
263 catch (...) {
264 return defaultreturn;
265 }
266 }
267
270 template <typename T, typename std::enable_if_t<!is_iterable_v<T> | is_cstring_v<T>>>
271 T retrieve(const std::string& key, const T& defaultreturn) const {
272 try {
273 return retrieve<T>(key);
274 }
275 catch (...) {
276 return defaultreturn;
277 }
278 }
279
281 friend std::ostream& operator<<(std::ostream& os, const RivetONNXrt& rort) {
282 os << "RivetONNXrt Network Summary: \n";
283 for (size_t i = 0; i < rort._inNames.size(); ++i) {
284 os << "- Input node " << i << " name: " << rort._inNames[i];
285 os << ", dimensions: (";
286 for (size_t j = 0; j < rort._inDims[i].size(); ++j) {
287 if (j) os << ", ";
288 os << rort._inDims[i][j];
289 }
290 os << "), type (as ONNX enums): " << rort._inTypes[i] << "\n";
291 }
292 for (size_t i = 0; i < rort._outNames.size(); ++i) {
293 os << "- Output node " << i << " name: " << rort._outNames[i];
294 os << ", dimensions: (";
295 for (size_t j = 0; j < rort._outDims[i].size(); ++j) {
296 if (j) os << ", ";
297 os << rort._outDims[i][j];
298 }
299 os << "), type (as ONNX enums): (" << rort._outTypes[i] << "\n";
300 }
301 return os;
302 }
303
305 Log& getLog() const {
306 string logname = "Rivet.RivetONNXrt." + _runName;
307 return Log::getLog(logname);
308 }
309
310
311 private:
312
314 void getNetworkInfo() {
315
316 Ort::AllocatorWithDefaultOptions allocator;
317
318 // Retrieve network metadata
319 _metadata = std::make_unique<Ort::ModelMetadata>(_session->GetModelMetadata());
320
321 // Find out how many input nodes the model expects
322 const size_t num_input_nodes = _session->GetInputCount();
323 _inDimsFlat.reserve(num_input_nodes);
324 _inTypes.reserve(num_input_nodes);
325 _inDims.reserve(num_input_nodes);
326 _inNames.reserve(num_input_nodes);
327 _inNamesPtr.reserve(num_input_nodes);
328 for (size_t i = 0; i < num_input_nodes; ++i) {
329 // Retrieve input node name
330 auto input_name = _session->GetInputNameAllocated(i, allocator);
331 _inNames.push_back(input_name.get());
332 _inNamesPtr.push_back(std::move(input_name));
333
334 // Retrieve input node type
335 auto in_type_info = _session->GetInputTypeInfo(i);
336 auto in_tensor_info = in_type_info.GetTensorTypeAndShapeInfo();
337 _inTypes.push_back(in_tensor_info.GetElementType());
338 _inDims.push_back(in_tensor_info.GetShape());
339 }
340
341 // Fix negative shape values - appears to be an artefact of batch size issues.
342 for (auto& dims : _inDims) {
343 int64_t n = 1;
344 for (auto& dim : dims) {
345 if (dim < 0) dim = abs(dim);
346 n *= dim;
347 }
348 _inDimsFlat.push_back(n);
349 }
350 // Find out how many output nodes the model expects
351 const size_t num_output_nodes = _session->GetOutputCount();
352 _outDimsFlat.reserve(num_output_nodes);
353 _outTypes.reserve(num_output_nodes);
354 _outDims.reserve(num_output_nodes);
355 _outNames.reserve(num_output_nodes);
356 _outNamesPtr.reserve(num_output_nodes);
357 for (size_t i = 0; i < num_output_nodes; ++i) {
358 // Retrieve output node name
359 auto output_name = _session->GetOutputNameAllocated(i, allocator);
360 _outNames.push_back(output_name.get());
361 _outNamesPtr.push_back(std::move(output_name));
362
363 // Retrieve output node type
364 auto out_type_info = _session->GetOutputTypeInfo(i);
365 if (out_type_info.GetONNXType() == ONNX_TYPE_TENSOR) {
366 auto out_tensor_info = out_type_info.GetTensorTypeAndShapeInfo();
367 _outTypes.push_back(out_tensor_info.GetElementType());
368 _outDims.push_back(out_tensor_info.GetShape());
369 }
370 else {
371 // Non-tensor output (e.g. Seq(Map) ZipMap from sklearn-onnx).
372 // UNDEFINED flags this node for computeMaps(); compute() will reject it.
373 _outTypes.push_back(ONNX_TENSOR_ELEMENT_DATA_TYPE_UNDEFINED);
374 _outDims.push_back({-1});
375 }
376 }
377
378 // Fix negative shape values - appears to be an artefact of batch size issues.
379 for (auto& dims : _outDims) {
380 int64_t n = 1;
381 for (auto& dim : dims) {
382 if (dim < 0) dim = abs(dim);
383 n *= dim;
384 }
385 _outDimsFlat.push_back(n);
386 }
387 }
388
389
391 void _checkTypes(const float*, size_t inode) const {
392 if (_inTypes[inode] != ONNX_TENSOR_ELEMENT_DATA_TYPE_FLOAT)
393 throw DataError("ONNX network provided wrong input type (float)");
394 }
396 void _checkTypes(const double*, size_t inode) const {
397 if (_inTypes[inode] != ONNX_TENSOR_ELEMENT_DATA_TYPE_DOUBLE)
398 throw DataError("ONNX network provided wrong input type (double)");
399 }
400
401 private:
402
404 std::string _runName;
405
407 std::unique_ptr<Ort::Env> _env;
408
410 std::unique_ptr<Ort::Session> _session;
411
413 std::unique_ptr<Ort::ModelMetadata> _metadata;
414
418 vector<vector<int64_t>> _inDims, _outDims;
419
421 vector<int64_t> _inDimsFlat, _outDimsFlat;
422
424 vector<ONNXTensorElementDataType> _inTypes, _outTypes;
425
427 vector<Ort::AllocatedStringPtr> _inNamesPtr, _outNamesPtr;
428
430 vector<const char*> _inNames, _outNames;
431 };
432
433
435 using RivetONNXrtPtr = unique_ptr<RivetONNXrt>;
436
437
441 inline string getONNXFilePath(const string& filename) {
443 const string path1 = findAnalysisDataFile(filename);
444 if (!path1.empty()) return path1;
445 throw Rivet::Error("Couldn't find an ONNX data file for '" + filename + "' " + "in the path "
447 }
448
449
458 inline RivetONNXrtPtr getONNX(const string& analysisname,
459 const string& suffix = "",
460 const string& extn = "onnx",
461 const int maxOrtThreads = 1) {
462 const string fname = analysisname + (suffix.empty() ? "" : "-") + suffix + "." + extn;
463 return make_unique<RivetONNXrt>(getONNXFilePath(fname), analysisname + "-onnxrt"s, maxOrtThreads);
464 }
465
466
470 using ONNXrtPtr = RivetONNXrtPtr;
472
473
474}
475
476#endif
Logging system for controlled & formatted writing to stdout.
Definition Logging.hh:10
static Log & getLog(const std::string &name)
Simple interface class to take care of basic ONNX networks.
Definition RivetONNXrt.hh:23
Log & getLog() const
Logger.
Definition RivetONNXrt.hh:305
RivetONNXrt(const string &filename, const string &runname="RivetONNXrt", const int maxOrtThreads=1)
Constructor.
Definition RivetONNXrt.hh:30
vector< T > compute(const vector< T > &inputs) const
Given a single-node input vector, populate and return the single-node output vector.
Definition RivetONNXrt.hh:135
T retrieve(const std::string &key, const T &defaultreturn) const
Definition RivetONNXrt.hh:271
std::string retrieve(const std::string &key) const
Template specialisation of retrieve for std::string.
Definition RivetONNXrt.hh:228
friend std::ostream & operator<<(std::ostream &os, const RivetONNXrt &rort)
Printing function for debugging.
Definition RivetONNXrt.hh:281
map< K, V > computeMap(const vector< V > &inputs) const
Single-node convenience overload: returns the map from a single Seq(Map(K,V)) output.
Definition RivetONNXrt.hh:197
vector< map< K, V > > computeMaps(const vector< vector< V > > &inputs) const
Definition RivetONNXrt.hh:148
vector< vector< T > > compute(const vector< vector< T > > &inputs) const
Definition RivetONNXrt.hh:83
vector< T > retrieve(const std::string &key, const vector< T > &defaultreturn) const
Overload of retrieve for vector<T>, with a default return.
Definition RivetONNXrt.hh:250
bool hasKey(const std::string &key) const
Method to check if key exists in network metatdata.
Definition RivetONNXrt.hh:206
T retrieve(const std::string &key) const
Definition RivetONNXrt.hh:215
vector< T > retrieve(const std::string &key) const
Overload of retrieve for vector<T>.
Definition RivetONNXrt.hh:239
#define MSG_DEBUG(x)
Debug messaging, not enabled by default, using MSG_LVL.
Definition Logging.hh:195
#define MSG_WARNING(x)
Warning messages for non-fatal bad things, using MSG_LVL.
Definition Logging.hh:200
#define MSG_ERROR(x)
Highest level messaging for serious problems, using MSG_LVL.
Definition Logging.hh:202
std::string findAnalysisDataFile(const std::string &filename, const std::vector< std::string > &pathprepend=std::vector< std::string >(), const std::vector< std::string > &pathappend=std::vector< std::string >())
Find the first file of the given name in the general data file search dirs.
std::string getRivetDataPath()
Get Rivet data install path.
T lexical_cast(const U &in)
Convert between any types via stringstream.
Definition Utils.hh:63
vector< string > split(const string &s, const string &sep)
Split a string on a specified separator string.
Definition Utils.hh:250
T getEnvParam(const std::string name, const T &fallback)
Get a parameter from a named environment variable, with automatic type conversion.
Definition Utils.hh:921
Definition LHCbCommon.hh:9
string getONNXFilePath(const string &filename)
Useful function for getting ONNX file paths.
Definition RivetONNXrt.hh:441
RivetONNXrtPtr getONNX(const string &analysisname, const string &suffix="", const string &extn="onnx", const int maxOrtThreads=1)
Definition RivetONNXrt.hh:458
std::enable_if_t< std::is_arithmetic_v< N1 > &&std::is_arithmetic_v< N2 >, signed_if_mixed_t< N1, N2 > > min(N1 a, N2 b)
Get the minimum of two numbers.
Definition MathUtils.hh:113
unique_ptr< RivetONNXrt > RivetONNXrtPtr
Typedef for a handle to an OONXrt object.
Definition RivetONNXrt.hh:435
std::string toString(const AnalysisInfo &ai)
String representation.
RivetONNXrt ONNXrt
Definition RivetONNXrt.hh:469
Error relating to provided data mismatching expectations.
Definition Exceptions.hh:89
Generic runtime Rivet error.
Definition Exceptions.hh:12