onnx_utilities.cpp
1 /*
2  * This file is part of CasADi.
3  *
4  * CasADi -- A symbolic framework for dynamic optimization.
5  * Copyright (C) 2010-2023 Joel Andersson, Joris Gillis, Moritz Diehl,
6  * KU Leuven. All rights reserved.
7  * Copyright (C) 2011-2014 Greg Horn
8  *
9  * CasADi is free software; you can redistribute it and/or
10  * modify it under the terms of the GNU Lesser General Public
11  * License as published by the Free Software Foundation; either
12  * version 3 of the License, or (at your option) any later version.
13  *
14  * CasADi is distributed in the hope that it will be useful,
15  * but WITHOUT ANY WARRANTY; without even the implied warranty of
16  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
17  * Lesser General Public License for more details.
18  *
19  * You should have received a copy of the GNU Lesser General Public
20  * License along with CasADi; if not, write to the Free Software
21  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
22  *
23  */
24 
25 
26 #include "onnx_model.hpp"
27 
29 namespace casadi {
30 
31  Sparsity Onnx::input_pattern(const onnx::GraphProto& graph, const std::string& name) const {
32  for (int i = 0; i < graph.sparse_initializer_size(); ++i) {
33  if (graph.sparse_initializer(i).values().name() == name) {
34  return sparse_tensor_to_dm(graph.sparse_initializer(i)).sparsity();
35  }
36  }
37  return Sparsity(); // null -> no overlay
38  }
39 
40  casadi_int get_int_attribute(const onnx::NodeProto& node, const std::string& name,
41  casadi_int default_value) {
42  for (int a = 0; a < node.attribute_size(); ++a) {
43  if (node.attribute(a).name() == name) return node.attribute(a).i();
44  }
45  return default_value;
46  }
47 
48  double get_float_attribute(const onnx::NodeProto& node, const std::string& name,
49  double default_value) {
50  for (int a = 0; a < node.attribute_size(); ++a) {
51  if (node.attribute(a).name() == name) return node.attribute(a).f();
52  }
53  return default_value;
54  }
55 
56  std::string get_string_attribute(const onnx::NodeProto& node, const std::string& name) {
57  for (int a = 0; a < node.attribute_size(); ++a) {
58  if (node.attribute(a).name() == name) return node.attribute(a).s();
59  }
60  return "";
61  }
62 
63  const onnx::GraphProto* get_graph_attribute(const onnx::NodeProto& node,
64  const std::string& name) {
65  for (int a = 0; a < node.attribute_size(); ++a) {
66  if (node.attribute(a).name() == name) return &node.attribute(a).g();
67  }
68  return nullptr;
69  }
70 
71  void Onnx::set_dimension(const std::string& name, casadi_int dim) {
72  dimension_overrides_[name] = dim;
73 
74  if (verbose_) {
75  uout() << "Set dimension override: " << name << " = " << dim << std::endl;
76  }
77  }
78 
79  casadi_int Onnx::get_dimension(
80  const onnx::TensorShapeProto& shape, int idx) const {
81  if (idx >= shape.dim_size()) return 1; // out of range / no dims -> scalar
82  const auto& dim = shape.dim(idx);
83 
84  if (dim.has_dim_value()) return static_cast<casadi_int>(dim.dim_value());
85 
86  if (dim.has_dim_param()) { // symbolic dimension -> resolve via overrides
87  std::string param_name = dim.dim_param();
88  auto it = dimension_overrides_.find(param_name);
89  casadi_assert(it != dimension_overrides_.end(),
90  "Symbolic dimension '" + param_name + "' not specified. " +
91  "Call set_dimension(\"" + param_name + "\", value) before create().");
92  return it->second;
93  }
94 
95  return 1; // no dimension info
96  }
97 
98  // Decode n raw_data values of type T into dm; return false if there's no raw_data
99  template<typename T>
100  static bool load_raw(DM& dm, casadi_int n, const onnx::TensorProto& tensor,
101  const char* type_name) {
102  if (!(tensor.has_raw_data() && tensor.raw_data().size() > 0)) return false;
103  const std::string& raw = tensor.raw_data();
104  casadi_assert(raw.size() == static_cast<size_t>(n) * sizeof(T),
105  std::string("Raw data size mismatch for ") + type_name);
106  const T* p = reinterpret_cast<const T*>(raw.data());
107  for (casadi_int i = 0; i < n; ++i) dm(i) = static_cast<double>(p[i]);
108  return true;
109  }
110 
111  DM Onnx::tensor_to_dm(const onnx::TensorProto& tensor) const {
112  std::vector<casadi_int> dims;
113  for (int i = 0; i < tensor.dims_size(); ++i) {
114  dims.push_back(static_cast<casadi_int>(tensor.dims(i)));
115  }
116 
117  if (verbose_) {
118  uout() << " tensor_to_dm: dims_size=" << tensor.dims_size()
119  << ", double_data_size=" << tensor.double_data_size()
120  << ", raw_data_size=" << tensor.raw_data().size()
121  << ", data_type=" << tensor.data_type();
122  if (dims.size() > 0) {
123  uout() << ", dims[0]=" << dims[0];
124  }
125  uout() << std::endl;
126  }
127 
128  // 2-D or less; higher-rank tensors are flattened to 2-D (columns absorb the extra axes).
129  casadi_int rows = dims.size() > 0 ? dims[0] : 1;
130  casadi_int cols = dims.size() > 1 ? dims[1] : 1;
131  if (dims.size() > 2) {
132  casadi_warning("ONNX tensor has " + std::to_string(dims.size()) +
133  " dimensions, flattening to 2D");
134  cols = 1;
135  for (size_t i = 1; i < dims.size(); ++i) {
136  cols *= dims[i];
137  }
138  }
139 
140  // Transpose-rep: a 2-D ONNX tensor (c,r) holds a CasADi (r,c) value's column-major bytes, so
141  // reading it back is just swapping the declared dims (the byte/fill order already matches).
142  if (dims.size() == 2) std::swap(rows, cols);
143 
144  DM dm(rows, cols); // DM is always double; ONNX integer/bool/float types are converted below
145 
146  onnx::TensorProto::DataType dtype =
147  static_cast<onnx::TensorProto::DataType>(tensor.data_type());
148 
149  casadi_int n = rows * cols;
150  switch (dtype) {
151  case onnx::TensorProto::DOUBLE: {
152  if (!load_raw<double>(dm, n, tensor, "DOUBLE")) {
153  casadi_assert(tensor.double_data_size() == n, "Tensor data size mismatch for DOUBLE");
154  for (casadi_int i = 0; i < n; ++i) dm(i) = tensor.double_data(i);
155  }
156  break;
157  }
158 
159  case onnx::TensorProto::FLOAT: { // 32-bit float -> promoted to double
160  if (!load_raw<float>(dm, n, tensor, "FLOAT")) {
161  casadi_assert(tensor.float_data_size() == n, "Tensor data size mismatch for FLOAT");
162  for (casadi_int i = 0; i < n; ++i) dm(i) = static_cast<double>(tensor.float_data(i));
163  }
164  break;
165  }
166 
167  case onnx::TensorProto::INT32: { // 32-bit integer -> double
168  if (!load_raw<int32_t>(dm, n, tensor, "INT32")) {
169  casadi_assert(tensor.int32_data_size() == n, "Tensor data size mismatch for INT32");
170  for (casadi_int i = 0; i < n; ++i) dm(i) = static_cast<double>(tensor.int32_data(i));
171  }
172  break;
173  }
174 
175  case onnx::TensorProto::INT64: { // 64-bit integer -> double
176  if (!load_raw<int64_t>(dm, n, tensor, "INT64")) {
177  casadi_assert(tensor.int64_data_size() == n, "Tensor data size mismatch for INT64");
178  for (casadi_int i = 0; i < n; ++i) dm(i) = static_cast<double>(tensor.int64_data(i));
179  }
180  break;
181  }
182 
183  case onnx::TensorProto::BOOL: {
184  // Boolean -> 0.0/1.0. ONNX stores BOOL in int32_data, or 1 byte per bool in raw_data.
185  if (tensor.int32_data_size() > 0) {
186  casadi_assert(tensor.int32_data_size() == n, "Tensor data size mismatch for BOOL");
187  for (casadi_int i = 0; i < n; ++i) dm(i) = tensor.int32_data(i) ? 1.0 : 0.0;
188  } else if (tensor.has_raw_data() && tensor.raw_data().size() > 0) {
189  const std::string& raw = tensor.raw_data();
190  casadi_assert(raw.size() == static_cast<size_t>(n), "Raw data size mismatch for BOOL");
191  for (casadi_int i = 0; i < n; ++i) dm(i) = raw[i] ? 1.0 : 0.0;
192  } else {
193  casadi_error("BOOL tensor has neither int32_data nor raw_data");
194  }
195  break;
196  }
197 
198  default:
199  casadi_error("Unsupported ONNX tensor data type: " + std::to_string(dtype) +
200  ". Supported types: DOUBLE(11), FLOAT(1), INT32(6), INT64(7), BOOL(9)");
201  }
202 
203  return dm;
204  }
205 
206  DM Onnx::sparse_tensor_to_dm(const onnx::SparseTensorProto& st) const {
207  // Dense shape (2-D; flatten any higher dims into the columns, like tensor_to_dm)
208  casadi_int nrow = st.dims_size() > 0 ? static_cast<casadi_int>(st.dims(0)) : 1;
209  casadi_int ncol = st.dims_size() > 1 ? static_cast<casadi_int>(st.dims(1)) : 1;
210  for (int i = 2; i < st.dims_size(); ++i) ncol *= static_cast<casadi_int>(st.dims(i));
211 
212  // Values: a 1-D [NNZ] tensor -> reuse the dense reader, then take its entries
213  std::vector<double> values = tensor_to_dm(st.values()).nonzeros();
214  casadi_int nnz = static_cast<casadi_int>(values.size());
215 
216  // Indices: INT64, either [NNZ,2] explicit (row,col) or [NNZ] row-major linearized
217  const onnx::TensorProto& it = st.indices();
218  std::vector<int64_t> flat;
219  if (it.int64_data_size() > 0) {
220  flat.assign(it.int64_data().begin(), it.int64_data().end());
221  } else if (it.has_raw_data() && !it.raw_data().empty()) {
222  const std::string& raw = it.raw_data();
223  const int64_t* p = reinterpret_cast<const int64_t*>(raw.data());
224  flat.assign(p, p + raw.size() / sizeof(int64_t));
225  }
226 
227  const bool coord = it.dims_size() == 2 && it.dims(1) == 2; // [NNZ,2] coordinate form
228  casadi_assert(static_cast<casadi_int>(flat.size()) == (coord ? 2 * nnz : nnz),
229  "Sparse tensor indices/values size mismatch");
230 
231  std::vector<casadi_int> rows(nnz), cols(nnz);
232  for (casadi_int k = 0; k < nnz; ++k) {
233  if (coord) {
234  rows[k] = static_cast<casadi_int>(flat[2 * k]);
235  cols[k] = static_cast<casadi_int>(flat[2 * k + 1]);
236  } else {
237  casadi_int lin = static_cast<casadi_int>(flat[k]); // row-major linearized
238  rows[k] = lin / ncol;
239  cols[k] = lin % ncol;
240  }
241  }
242  // Transpose-rep: the stored COO is V^T (dims (c,r), coords (col,row)); build it then
243  // transpose to recover the CasADi value V. DM::triplet is order-agnostic.
244  return DM::triplet(rows, cols, values, nrow, ncol).T();
245  }
246 
247 } // namespace casadi
bool verbose_
Verbose – for debugging.
std::vector< Scalar > & nonzeros()
Matrix< Scalar > T() const
Transpose the matrix.
const Sparsity & sparsity() const
Const access the sparsity - reference to data member.
static Matrix< double > triplet(const std::vector< casadi_int > &row, const std::vector< casadi_int > &col, const Matrix< double > &d)
Construct a sparse matrix from triplet form.
std::map< std::string, casadi_int > dimension_overrides_
Dimension overrides for symbolic dimensions.
Definition: onnx_model.hpp:113
void set_dimension(const std::string &name, casadi_int dim)
Set dimension for a symbolic variable.
The casadi namespace.
Definition: archiver.cpp:28
double get_float_attribute(const onnx::NodeProto &node, const std::string &name, double default_value)
Read a float node attribute by name, or default_value if absent.
const onnx::GraphProto * get_graph_attribute(const onnx::NodeProto &node, const std::string &name)
Read a subgraph (GraphProto) node attribute by name, or nullptr if absent.
casadi_int get_int_attribute(const onnx::NodeProto &node, const std::string &name, casadi_int default_value)
Read an integer node attribute by name, or default_value if absent.
std::string get_string_attribute(const onnx::NodeProto &node, const std::string &name)
Read a string node attribute by name, or "" if absent.
Matrix< double > DM
Definition: dm_fwd.hpp:33
static bool load_raw(DM &dm, casadi_int n, const onnx::TensorProto &tensor, const char *type_name)
std::ostream & uout()