26 #include "onnx_model.hpp"
28 #include <onnx/shape_inference/implementation.h>
34 static bool integer_tensor(
const onnx::TensorProto& tensor, std::vector<int64_t>& values) {
35 if (tensor.data_type() != onnx::TensorProto::INT64 &&
36 tensor.data_type() != onnx::TensorProto::INT32)
return false;
38 if (tensor.has_raw_data()) {
39 const auto& raw = tensor.raw_data();
40 size_t width = tensor.data_type() == onnx::TensorProto::INT64 ? 8 : 4;
41 casadi_assert(raw.size() % width == 0,
"Invalid integer tensor raw_data size");
42 for (
size_t i = 0; i < raw.size(); i += width) {
44 for (
size_t j = 0; j < width; ++j) {
45 bits |=
static_cast<uint64_t
>(
static_cast<unsigned char>(raw[i + j])) << (8 * j);
49 values.push_back(
static_cast<int64_t
>(bits) -
static_cast<int64_t
>((bits >> 31) << 32));
51 values.push_back(bits >> 63 ? -1 -
static_cast<int64_t
>(~bits) :
52 static_cast<int64_t
>(bits));
55 }
else if (tensor.data_type() == onnx::TensorProto::INT64) {
56 values.assign(tensor.int64_data().begin(), tensor.int64_data().end());
58 values.assign(tensor.int32_data().begin(), tensor.int32_data().end());
65 return (a.
size1() == b.size1() || a.
size1() == 1 || b.size1() == 1)
66 && (a.
size2() == b.size2() || a.
size2() == 1 || b.size2() == 1);
71 if (!v->is_vector())
return;
89 if (x->is_scalar() || y->is_scalar())
return;
93 for (
MX* v : {x, y}) {
95 + x->dim() +
" with " + y->dim());
96 if (v->size1() != nr || v->size2() != nc) *v = repmat(*v, nr / v->size1(), nc / v->size2());
101 void Onnx::process_graph_initializers(
102 const onnx::GraphProto& graph,
103 std::map<std::string, MX>& value_map,
104 bool verbose)
const {
106 for (
int i = 0; i < graph.initializer_size(); ++i) {
107 const onnx::TensorProto& tensor = graph.initializer(i);
108 std::string tensor_name = tensor.name();
111 uout() <<
" Processing initializer: " << tensor_name << std::endl;
114 value_map[tensor_name] = MX(tensor_to_dm(tensor));
119 void Onnx::process_graph_inputs(
120 const onnx::GraphProto& graph,
121 std::map<std::string, MX>& value_map,
122 std::vector<MX>& func_inputs,
123 std::vector<std::string>& input_names,
124 bool verbose)
const {
126 for (
int i = 0; i < graph.input_size(); ++i) {
127 const onnx::ValueInfoProto& input = graph.input(i);
128 std::string input_name = input.name();
131 if (value_map.count(input_name)) {
133 uout() <<
" Skipping input '" << input_name
134 <<
"' (it's an initializer)" << std::endl;
140 const onnx::TensorShapeProto& shape =
141 input.type().tensor_type().shape();
142 casadi_int rows = get_dimension(shape, 0);
143 casadi_int cols = get_dimension(shape, 1);
146 uout() <<
" Creating input: " << input_name
147 <<
" [" << rows <<
", " << cols <<
"]" << std::endl;
157 Sparsity ov = input_pattern(graph, input_name);
158 MX mx_input = !ov.is_null() ?
MX::sym(input_name, ov)
159 : shape.dim_size() < 2 ? MX::sym(input_name, rows, cols)
160 : MX::sym(input_name, cols, rows);
161 value_map[input_name] = mx_input;
162 func_inputs.push_back(mx_input);
163 input_names.push_back(input_name);
168 static std::map<std::string, casadi_int>
declared_ranks(
const onnx::GraphProto& graph) {
169 std::map<std::string, casadi_int> ranks;
170 for (
const auto* infos : {&graph.input(), &graph.value_info(), &graph.output()}) {
171 for (
const auto& info : *infos) {
172 if (info.type().tensor_type().has_shape()) {
173 ranks[info.name()] = info.type().tensor_type().shape().dim_size();
177 for (
const auto& tensor : graph.initializer()) ranks[tensor.name()] = tensor.dims_size();
186 const std::map<std::string, casadi_int>& ranks, onnx::NodeProto*
copy) {
187 if (node.op_type() ==
"OneHot" || node.input_size() == 0)
return node;
188 for (
int a = 0; a < node.attribute_size(); ++a) {
189 if (node.attribute(a).name() !=
"axis" || node.attribute(a).i() >= 0)
continue;
190 auto it = ranks.find(node.input(0));
191 if (it == ranks.end())
return node;
193 copy->mutable_attribute(a)->set_i(node.attribute(a).i() + it->second);
200 void Onnx::process_graph_nodes(
201 const onnx::GraphProto& graph,
202 std::map<std::string, MX>& value_map,
205 IntegerConstants integer_constants;
206 for (
const auto& tensor : graph.initializer()) {
207 std::vector<int64_t> values;
208 if (
integer_tensor(tensor, values)) integer_constants[tensor.name()] = std::move(values);
214 std::map<std::string, MX> kron_operand_a_, kron_operand_b_;
216 for (
int i = 0; i < graph.node_size(); ++i) {
217 onnx::NodeProto resolved;
219 std::string op_type = node.op_type();
222 uout() <<
" Processing node " << i <<
": " << op_type << std::endl;
226 if (op_type ==
"Constant" && node.output_size() == 1) {
227 for (
const auto& attr : node.attribute()) {
228 std::vector<int64_t> values;
229 if (attr.name() ==
"value_int") {
231 }
else if (attr.name() ==
"value_ints") {
232 values.assign(attr.ints().begin(), attr.ints().end());
233 }
else if (attr.name() !=
"value" || !
integer_tensor(attr.t(), values)) {
236 integer_constants[node.output(0)] = std::move(values);
250 if (node.name().rfind(
"kron", 0) == 0) {
251 const std::string& nm = node.name();
252 std::size_t us = nm.rfind(
'_');
253 std::string tag = nm.substr(0, us), role = nm.substr(us + 1);
255 kron_operand_a_[tag] = value_map.at(node.input(0));
259 kron_operand_b_[tag] = value_map.at(node.input(0));
262 if (role ==
"P")
continue;
265 casadi_assert(kron_operand_a_.count(tag) && kron_operand_b_.count(tag),
266 "ONNX import: incomplete kron node group '" + tag +
"'");
267 value_map[node.output(0)] =
MX::kron(kron_operand_a_.at(tag), kron_operand_b_.at(tag));
273 std::vector<MX> node_inputs;
274 for (
int j = 0; j < node.input_size(); ++j) {
275 std::string input_name = node.input(j);
278 if (input_name.empty()) {
279 node_inputs.push_back(MX());
283 casadi_assert(value_map.count(input_name),
284 "Unknown input tensor '" + input_name +
285 "' required by node " + std::to_string(i) +
286 " (op_type: " + op_type +
")");
287 node_inputs.push_back(value_map[input_name]);
294 if (op_type ==
"Split") {
295 casadi_assert(node_inputs.size() >= 1,
"Split requires 1 input");
297 casadi_assert(axis == 0 || axis == 1,
"Split: only axis 0 and 1 supported");
300 std::vector<casadi_int> split_sizes;
301 if (node_inputs.size() >= 2) {
302 DM split_dm =
DM(node_inputs[1]);
303 for (casadi_int k = 0; k < split_dm.nnz(); ++k) {
304 split_sizes.push_back(
static_cast<casadi_int
>(
static_cast<double>(split_dm.nz(k))));
307 for (
int a = 0; a < node.attribute_size(); ++a) {
308 if (node.attribute(a).name() ==
"split") {
309 for (
int k = 0; k < node.attribute(a).ints_size(); ++k) {
310 split_sizes.push_back(node.attribute(a).ints(k));
316 if (split_sizes.empty()) {
318 casadi_int total = (axis == 0) ? node_inputs[0].size2() : node_inputs[0].size1();
319 split_sizes.assign(node.output_size(), total / node.output_size());
323 std::vector<casadi_int> offset = {0};
324 for (casadi_int sz : split_sizes) offset.push_back(offset.back() + sz);
325 std::vector<MX> outputs = (axis == 0) ? horzsplit(node_inputs[0], offset)
326 : vertsplit(node_inputs[0], offset);
328 for (casadi_int j = 0; j < outputs.size(); ++j) value_map[node.output(j)] = outputs[j];
332 }
else if (op_type ==
"Scan") {
334 casadi_assert(body !=
nullptr,
"Scan node requires a 'body' subgraph");
335 casadi_int num_scan_inputs =
get_int_attribute(node,
"num_scan_inputs", node.input_size());
336 casadi_int M = node.input_size() - num_scan_inputs;
339 std::set<std::string> body_in_names, defined;
340 for (
int b = 0;
b < body->input_size(); ++
b) {
341 body_in_names.insert(body->input(b).name());
342 defined.insert(body->input(b).name());
344 for (
int nn = 0; nn < body->node_size(); ++nn)
345 for (
int oo = 0; oo < body->node(nn).output_size(); ++oo)
346 defined.insert(body->node(nn).output(oo));
347 bool has_capture =
false;
348 for (
int nn = 0; nn < body->node_size() && !has_capture; ++nn)
349 for (
int ii = 0; ii < body->node(nn).input_size(); ++ii) {
350 const std::string& in = body->node(nn).input(ii);
351 if (!in.empty() && !defined.count(in)) { has_capture =
true;
break; }
354 if (M == 0 && !has_capture) {
357 Function base = function_from_graph(*body, op_type +
"_body");
358 casadi_int n = node_inputs[0].size2() / base.size2_in(0);
359 std::vector<MX> outputs =
360 base.map(n)(std::vector<MX>(node_inputs.begin(), node_inputs.end()));
361 for (
int j = 0; j < node.output_size(); ++j) value_map[node.output(j)] = outputs[j];
367 const onnx::NodeProto* call =
nullptr;
368 for (
int nn = 0; nn < body->node_size(); ++nn)
369 if (!body->node(nn).domain().empty()) { call = &body->node(nn);
break; }
370 casadi_assert(call,
"reduce-map Scan body must contain a base function call");
371 casadi_int nin = call->input_size(), nout = call->output_size();
374 std::vector<bool> reduce_in(nin), reduce_out(nout,
false);
375 for (casadi_int j = 0; j < nin; ++j) reduce_in[j] = !body_in_names.count(call->input(j));
376 for (
int nn = 0; nn < body->node_size(); ++nn) {
377 if (body->node(nn).op_type() !=
"Add")
continue;
378 for (
int ii = 0; ii < body->node(nn).input_size(); ++ii)
379 for (casadi_int j = 0; j < nout; ++j)
380 if (body->node(nn).input(ii) == call->output(j)) reduce_out[j] =
true;
384 std::vector<std::pair<casadi_int, casadi_int>> in_shapes(nin);
385 for (casadi_int j = 0; j < nin; ++j) {
387 MX cap = value_map.at(call->input(j));
388 in_shapes[j] = {cap.size1(), cap.size2()};
390 for (
int b = 0;
b < body->input_size(); ++
b)
391 if (body->input(b).name() == call->input(j)) {
392 const onnx::TensorShapeProto& sh = body->input(b).type().tensor_type().shape();
393 in_shapes[j] = {get_dimension(sh, 0), get_dimension(sh, 1)};
399 const onnx::FunctionProto* fp = find_function(call->op_type(), call->domain());
400 casadi_assert(fp,
"reduce-map base function '" + call->op_type() +
"' not found");
401 Function base = function_from_function_proto(*fp, in_shapes, op_type +
"_base");
404 std::vector<MX> args(nin);
405 casadi_int n = 0, r = 0;
406 for (casadi_int j = 0; j < nin; ++j) {
408 args[j] = value_map.at(call->input(j));
410 args[j] = node_inputs[M + r];
411 if (n == 0) n = args[j].size2() / base.size2_in(j);
415 casadi_assert(n > 0,
"reduce-map import: could not infer map size");
416 std::vector<MX> outs = base.map(n, reduce_in, reduce_out)(args);
419 casadi_int si = 0, ci = 0;
420 for (casadi_int j = 0; j < nout; ++j) {
421 if (reduce_out[j]) value_map[node.output(si++)] = outs[j];
423 value_map[node.output(M + ci++)] = outs[j];
428 }
else if (op_type ==
"If") {
431 casadi_assert(then_b && else_b,
"If node requires then_branch and else_branch");
432 std::vector<MX> t = eval_captured_subgraph(*then_b, value_map);
433 std::vector<MX> e = eval_captured_subgraph(*else_b, value_map);
434 MX cond = node_inputs[0];
435 for (
int j = 0; j < node.output_size(); ++j) {
436 value_map[node.output(j)] =
if_else(cond, t[j], e[j]);
441 }
else if (op_type ==
"Loop") {
442 casadi_error(
"ONNX import: 'Loop' control flow operator is not supported.");
447 std::string node_domain = node.domain();
448 if (!node_domain.empty()) {
449 const onnx::FunctionProto* func_proto = find_function(op_type, node_domain);
451 if (func_proto !=
nullptr) {
453 uout() <<
" Function call to: " << node_domain <<
"." << op_type << std::endl;
458 onnx::GraphProto func_graph;
459 func_graph.set_name(func_proto->name());
460 for (
int n = 0; n < func_proto->node_size(); ++n) {
461 *func_graph.add_node() = func_proto->node(n);
465 std::map<std::string, MX> func_value_map;
466 for (
size_t n = 0; n < node_inputs.size() && n < func_proto->input_size(); ++n) {
467 func_value_map[func_proto->input(n)] = node_inputs[n];
470 process_graph_nodes(func_graph, func_value_map,
false);
473 for (
int n = 0; n < node.output_size() && n < func_proto->output_size(); ++n) {
474 std::string func_output_name = func_proto->output(n);
475 casadi_assert(func_value_map.count(func_output_name),
476 "Function output '" + func_output_name +
"' not found");
477 value_map[node.output(n)] = func_value_map[func_output_name];
480 uout() <<
" -> " << node.output(n) << std::endl;
491 output = process_node_operation(op_type, node, node_inputs, integer_constants);
495 casadi_assert(node.output_size() >= 1,
496 "Node must have at least one output");
498 std::string output_name = node.output(0);
499 value_map[output_name] = output;
502 uout() <<
" -> " << output_name <<
" " << output.dim() << std::endl;
508 void Onnx::collect_graph_outputs(
509 const onnx::GraphProto& graph,
510 const std::map<std::string, MX>& value_map,
511 std::vector<MX>& func_outputs,
512 std::vector<std::string>& output_names,
513 bool verbose)
const {
515 for (
int i = 0; i < graph.output_size(); ++i) {
516 const onnx::ValueInfoProto& output = graph.output(i);
517 std::string output_name = output.name();
519 casadi_assert(value_map.count(output_name),
520 "Unknown output tensor: " + output_name +
521 ". This usually means the ONNX graph contains unsupported operations.");
525 func_outputs.push_back(value_map.at(output_name));
526 output_names.push_back(output_name);
529 uout() <<
" Graph output: " << output_name << std::endl;
534 const onnx::FunctionProto* Onnx::find_function(
const std::string& name,
535 const std::string& domain)
const {
536 for (
int f = 0; f <
model_.functions_size(); ++f)
537 if (
model_.functions(f).name() == name &&
model_.functions(f).domain() == domain)
538 return &
model_.functions(f);
545 for (
const auto& node : graph.node()) {
546 if (node.op_type() ==
"OneHot" || node.input_size() == 0)
continue;
547 if (
get_int_attribute(node,
"axis", 0) < 0 && !ranks.count(node.input(0)))
return true;
553 casadi_assert(
has_model_,
"No ONNX model loaded. Call load() first.");
555 return function_from_graph(
model_.graph(), name);
560 onnx::ModelProto inferred =
model_;
562 onnx::shape_inference::InferShapes(inferred, onnx::OpSchemaRegistry::Instance(),
563 onnx::ShapeInferenceOptions(
false, 0,
true));
564 }
catch (
const std::exception& e) {
565 if (
verbose_)
uout() <<
"ONNX shape inference failed: " << e.what() << std::endl;
566 return function_from_graph(
model_.graph(), name);
568 return function_from_graph(inferred.graph(), name);
571 Function Onnx::function_from_graph(
const onnx::GraphProto& graph,
572 const std::string& name) {
573 std::map<std::string, MX> value_map;
574 std::vector<MX> inputs, outputs;
575 std::vector<std::string> input_names, output_names;
578 uout() <<
"Building CasADi Function '" << name <<
"' from graph '" << graph.name()
579 <<
"' (" << graph.initializer_size() <<
" initializers, "
580 << graph.input_size() <<
" inputs, " << graph.output_size() <<
" outputs, "
581 << graph.node_size() <<
" nodes)" << std::endl;
584 process_graph_initializers(graph, value_map,
verbose_);
585 process_graph_inputs(graph, value_map, inputs, input_names,
verbose_);
586 process_graph_nodes(graph, value_map,
verbose_);
587 collect_graph_outputs(graph, value_map, outputs, output_names,
verbose_);
589 return Function(name, inputs, outputs, input_names, output_names);
592 Function Onnx::function_from_function_proto(
593 const onnx::FunctionProto& fp,
594 const std::vector<std::pair<casadi_int, casadi_int>>& in_shapes,
595 const std::string& name) {
598 g.set_name(fp.name());
599 for (
int j = 0; j < fp.input_size(); ++j) {
600 onnx::ValueInfoProto* vi = g.add_input();
601 vi->set_name(fp.input(j));
602 onnx::TypeProto::Tensor* tt = vi->mutable_type()->mutable_tensor_type();
603 tt->set_elem_type(real_type());
604 tt->mutable_shape()->add_dim()->set_dim_value(in_shapes[j].first);
605 tt->mutable_shape()->add_dim()->set_dim_value(in_shapes[j].second);
607 for (
int k = 0; k < fp.node_size(); ++k) *g.add_node() = fp.node(k);
608 for (
int j = 0; j < fp.output_size(); ++j) g.add_output()->set_name(fp.output(j));
609 return function_from_graph(g, name);
612 std::vector<MX> Onnx::eval_captured_subgraph(
const onnx::GraphProto& graph,
613 std::map<std::string, MX> scope) {
615 process_graph_initializers(graph, scope,
verbose_);
616 process_graph_nodes(graph, scope,
verbose_);
617 std::vector<MX> outputs;
618 for (
int i = 0; i < graph.output_size(); ++i)
619 outputs.push_back(scope.at(graph.output(i).name()));
626 casadi_assert(m.
is_constant(),
"Expected a constant integer tensor");
627 DM dm =
static_cast<DM>(m);
628 std::vector<casadi_int> v;
629 for (casadi_int k = 0; k < dm.
numel(); ++k)
630 v.push_back(
static_cast<casadi_int
>(dm(k).scalar()));
637 return if_else(x > 40, x, log1p(exp(fmin(x, 50))));
642 return 0.5 * (1 + tanh(0.5 * x));
647 return if_else(x > 0, x, alpha * expm1(fmin(x, 1)));
651 static const std::set<std::string> ops = {
"Relu",
"LeakyRelu",
"PRelu",
"Sigmoid",
652 "HardSigmoid",
"HardSwish",
"Softplus",
"Softsign",
"Elu",
"Selu",
"Celu",
"Mish",
"Gelu",
653 "Clip",
"ThresholdedRelu",
"Shrink",
"Swish"};
654 return ops.count(op_type) > 0;
660 static MX activation(
const std::string& op_type,
const onnx::NodeProto& node,
661 const std::vector<MX>& node_inputs) {
662 casadi_assert(node_inputs.size() >= 1, op_type +
" requires 1 input");
663 const MX& x = node_inputs[0];
665 if (op_type ==
"Relu") {
667 }
else if (op_type ==
"LeakyRelu") {
669 }
else if (op_type ==
"PRelu") {
670 casadi_assert(node_inputs.size() >= 2,
"PRelu requires 2 inputs");
671 MX bx = x, slope = node_inputs[1];
673 return if_else(bx >= 0, bx, slope * bx);
674 }
else if (op_type ==
"ThresholdedRelu") {
676 }
else if (op_type ==
"Shrink") {
679 return if_else(x < -lambd, x + bias,
if_else(x > lambd, x - bias, 0));
680 }
else if (op_type ==
"Sigmoid") {
682 }
else if (op_type ==
"Swish") {
684 }
else if (op_type ==
"HardSigmoid") {
687 return fmax(0, fmin(1, alpha * x + beta));
688 }
else if (op_type ==
"HardSwish") {
689 return x * fmax(0, fmin(1, x / 6 + 0.5));
690 }
else if (op_type ==
"Softplus") {
692 }
else if (op_type ==
"Softsign") {
693 return x / (1 + fabs(x));
694 }
else if (op_type ==
"Elu") {
696 }
else if (op_type ==
"Selu") {
699 return gamma *
elu(x, alpha);
700 }
else if (op_type ==
"Celu") {
704 casadi_assert(alpha != 0,
"Celu requires a nonzero alpha");
705 return if_else(x > 0, x, alpha * expm1(fmin(x, std::fabs(alpha)) / alpha));
706 }
else if (op_type ==
"Mish") {
708 }
else if (op_type ==
"Gelu") {
710 if (approximate.empty() || approximate ==
"none") {
711 return 0.5 * x * (1 + erf(x / std::sqrt(2.0)));
713 casadi_assert(approximate ==
"tanh",
714 "Gelu: unsupported approximate '" + approximate +
"'");
715 return 0.5 * x * (1 + tanh(std::sqrt(2 /
pi) * (x + 0.044715 * pow(x, 3))));
716 }
else if (op_type ==
"Clip") {
720 for (
size_t k = 1; k <= 2; ++k) {
722 if (node_inputs.size() > k) {
723 bound = node_inputs[k];
725 double inf = std::numeric_limits<double>::infinity();
727 if (!std::isinf(b)) bound = b;
730 output = k == 1 ? fmax(output, bound) : fmin(output, bound);
734 casadi_error(
"Unsupported activation '" + op_type +
"'");
737 MX Onnx::process_node_operation(
738 const std::string& op_type,
739 const onnx::NodeProto& node,
740 const std::vector<MX>& node_inputs,
741 const IntegerConstants& integer_constants) {
743 auto input_ints = [&](
size_t i) -> std::vector<casadi_int> {
744 auto it = integer_constants.find(node.input(i));
745 if (it == integer_constants.end())
return constant_ints(node_inputs[i]);
746 std::vector<casadi_int> values;
747 for (int64_t v : it->second) {
748 casadi_assert(v >= std::numeric_limits<casadi_int>::min() &&
749 v <= std::numeric_limits<casadi_int>::max(),
"Integer constant out of range: " +
str(v));
750 values.push_back(
static_cast<casadi_int
>(v));
760 const MX& x = node_inputs[0];
761 if (mapping->arity == 1) {
762 casadi_assert(node_inputs.size() >= 1, op_type +
" requires 1 input");
763 switch (mapping->casadi_op) {
764 case OP_SIN:
return sin(x);
765 case OP_COS:
return cos(x);
766 case OP_TAN:
return tan(x);
776 case OP_EXP:
return exp(x);
777 case OP_LOG:
return log(x);
784 case OP_ERF:
return erf(x);
785 case OP_INV:
return 1.0 / x;
794 }
else if (mapping->arity == 2) {
795 casadi_assert(node_inputs.size() >= 2, op_type +
" requires 2 inputs");
796 MX bx = x, by = node_inputs[1];
798 switch (mapping->casadi_op) {
799 case OP_ADD:
return bx + by;
800 case OP_SUB:
return bx - by;
801 case OP_MUL:
return bx * by;
802 case OP_DIV:
return bx / by;
803 case OP_POW:
return pow(bx, by);
811 if (op_type ==
"Less") {
812 casadi_assert(node_inputs.size() >= 2,
"Less requires 2 inputs");
813 output =
if_else(node_inputs[0] < node_inputs[1], MX(1.0), MX(0.0));
815 }
else if (op_type ==
"Equal") {
816 casadi_assert(node_inputs.size() >= 2,
"Equal requires 2 inputs");
817 output = !ne(node_inputs[0], node_inputs[1]);
819 }
else if (op_type ==
"LessOrEqual") {
820 casadi_assert(node_inputs.size() >= 2,
"LessOrEqual requires 2 inputs");
821 output =
if_else(node_inputs[0] <= node_inputs[1], MX(1.0), MX(0.0));
823 }
else if (op_type ==
"Greater") {
824 casadi_assert(node_inputs.size() >= 2,
"Greater requires 2 inputs");
825 output =
if_else(node_inputs[0] > node_inputs[1], MX(1.0), MX(0.0));
827 }
else if (op_type ==
"GreaterOrEqual") {
828 casadi_assert(node_inputs.size() >= 2,
"GreaterOrEqual requires 2 inputs");
829 output =
if_else(node_inputs[0] >= node_inputs[1], MX(1.0), MX(0.0));
831 }
else if (op_type ==
"Min") {
832 casadi_assert(node_inputs.size() >= 2,
"Min requires 2 inputs");
833 output = fmin(node_inputs[0], node_inputs[1]);
835 }
else if (op_type ==
"Max") {
836 casadi_assert(node_inputs.size() >= 2,
"Max requires 2 inputs");
837 output = fmax(node_inputs[0], node_inputs[1]);
839 }
else if (op_type ==
"Mod") {
840 casadi_assert(node_inputs.size() >= 2,
"Mod requires 2 inputs");
841 output = fmod(node_inputs[0], node_inputs[1]);
843 }
else if (op_type ==
"ReduceSum") {
845 casadi_assert(node_inputs.size() >= 1,
"ReduceSum requires 1 input");
846 output = sum1(sum2(node_inputs[0]));
848 }
else if (op_type ==
"Not") {
849 casadi_assert(node_inputs.size() >= 1,
"Not requires 1 input");
850 output = logic_not(node_inputs[0]);
852 }
else if (op_type ==
"And") {
853 casadi_assert(node_inputs.size() >= 2,
"And requires 2 inputs");
854 output = logic_and(node_inputs[0], node_inputs[1]);
856 }
else if (op_type ==
"Or") {
857 casadi_assert(node_inputs.size() >= 2,
"Or requires 2 inputs");
858 output = logic_or(node_inputs[0], node_inputs[1]);
860 }
else if (op_type ==
"Where") {
861 casadi_assert(node_inputs.size() >= 3,
"Where requires 3 inputs");
862 output =
if_else(node_inputs[0], node_inputs[1], node_inputs[2]);
864 }
else if (op_type ==
"Identity") {
865 casadi_assert(node_inputs.size() >= 1,
"Identity requires 1 input");
866 output = node_inputs[0];
868 }
else if (op_type ==
"Cast") {
871 casadi_assert(node_inputs.size() >= 1,
"Cast requires 1 input");
872 output = node_inputs[0];
874 }
else if (op_type ==
"MatMul") {
876 casadi_assert(node_inputs.size() >= 2,
"MatMul requires 2 inputs");
877 output = mtimes(node_inputs[1], node_inputs[0]);
879 }
else if (op_type ==
"Gemm") {
882 casadi_assert(node_inputs.size() >= 2,
"Gemm requires at least 2 inputs");
883 MX A =
get_int_attribute(node,
"transB", 0) ? node_inputs[1].T() : node_inputs[1];
884 MX B =
get_int_attribute(node,
"transA", 0) ? node_inputs[0].T() : node_inputs[0];
886 if (node_inputs.size() >= 3) {
887 MX
C = node_inputs[2];
892 }
else if (op_type ==
"Sum") {
894 casadi_assert(node_inputs.size() >= 1,
"Sum requires at least 1 input");
895 output = node_inputs[0];
896 for (casadi_int idx = 1; idx < node_inputs.size(); ++idx) output = output + node_inputs[idx];
898 }
else if (op_type ==
"Pad") {
902 casadi_assert(node_inputs.size() >= 2,
"Pad requires data and pads");
903 MX block = node_inputs[0];
904 std::vector<casadi_int> pads =
constant_ints(node_inputs[1]);
905 casadi_int col_off = pads[0], row_off = pads[1];
906 casadi_int br = block.size1(), bc = block.size2();
907 casadi_int
C = col_off + bc + pads[2];
912 if (col_off > 0 || pads[2] > 0) {
913 padded = horzcat(MX(Sparsity(br, col_off)), padded, MX(Sparsity(br, pads[2])));
915 if (row_off > 0 || pads[3] > 0) {
916 padded = vertcat(MX(Sparsity(row_off, C)), padded, MX(Sparsity(pads[3], C)));
920 }
else if (op_type ==
"Einsum") {
923 casadi_assert(node_inputs.size() >= 2,
"Einsum requires 2 inputs");
926 size_t comma = eq.find(
','), arrow = eq.find(
"->");
927 casadi_assert(comma != std::string::npos && arrow != std::string::npos,
928 "ONNX import: only binary Einsum 'a,b->c' is supported");
929 std::string sa = eq.substr(0, comma);
930 std::string sb = eq.substr(comma + 1, arrow - comma - 1);
931 std::string sc = eq.substr(arrow + 2);
937 std::reverse(sa.begin(), sa.end());
938 std::reverse(sb.begin(), sb.end());
939 std::reverse(sc.begin(), sc.end());
945 std::map<char, casadi_int> lsize;
946 for (
int t = 0; t < 2; ++t) {
947 const std::string& s = (t == 0) ? sa : sb;
948 const MX& m = node_inputs[t];
950 lsize[s[0]] = m.numel();
951 }
else if (s.size() >= 2) {
952 lsize[s[0]] = m.size1();
953 lsize[s[1]] = m.size2();
957 std::map<char, casadi_int> lab;
958 casadi_int next_label = -1;
959 for (
char ch : sa + sb + sc)
if (!lab.count(ch)) lab[ch] = next_label--;
962 std::vector<casadi_int> da, db, dc, La, Lb, Lc;
963 for (
char ch : sa) { da.push_back(lsize[ch]); La.push_back(lab[ch]); }
964 for (
char ch : sb) { db.push_back(lsize[ch]); Lb.push_back(lab[ch]); }
965 for (
char ch : sc) { dc.push_back(lsize[ch]); Lc.push_back(lab[ch]); }
966 output = einstein(vec(node_inputs[0]), vec(node_inputs[1]), da, db, dc, La, Lb, Lc);
968 }
else if (op_type ==
"Det") {
969 casadi_assert(node_inputs.size() >= 1,
"Det requires 1 input");
970 output = det(node_inputs[0]);
972 }
else if (op_type ==
"ReduceLogSumExp") {
974 casadi_assert(node_inputs.size() >= 1,
"ReduceLogSumExp requires 1 input");
975 output = log(sum1(sum2(exp(node_inputs[0]))));
977 }
else if (op_type ==
"Constant") {
979 const onnx::AttributeProto* value_attr =
nullptr;
980 for (
int a = 0; a < node.attribute_size(); ++a) {
981 const auto& attr = node.attribute(a);
982 const std::string& an = attr.name();
983 if (an ==
"value" || an ==
"sparse_value" || an ==
"value_int" ||
984 an ==
"value_ints" || an ==
"value_float" || an ==
"value_floats") {
985 casadi_assert(value_attr ==
nullptr,
"Constant node has multiple value attributes");
989 casadi_assert(value_attr !=
nullptr,
990 "Constant node requires a numeric value or sparse_value attribute");
991 const auto& attr = *value_attr;
992 if (attr.name() ==
"sparse_value") {
993 output = MX(sparse_tensor_to_dm(attr.sparse_tensor()));
994 }
else if (attr.name() ==
"value") {
995 output = MX(tensor_to_dm(attr.t()));
996 }
else if (attr.name() ==
"value_int") {
997 output = MX(
static_cast<double>(attr.i()));
998 }
else if (attr.name() ==
"value_float") {
999 output = MX(
static_cast<double>(attr.f()));
1001 std::vector<double> values;
1002 if (attr.name() ==
"value_ints") {
1003 for (
auto v : attr.ints()) values.push_back(
static_cast<double>(v));
1005 for (
auto v : attr.floats()) values.push_back(
static_cast<double>(v));
1007 output = MX(
DM(values));
1011 }
else if (op_type ==
"Reshape") {
1012 casadi_assert(node_inputs.size() >= 2,
1013 "Reshape operation requires 2 inputs (data and shape)");
1015 casadi_assert(node_inputs[1].is_constant(),
1016 "Reshape shape must be a constant");
1017 std::vector<casadi_int> shape = input_ints(1);
1023 if (shape.size() > 2) {
1025 output = densify(node_inputs[0]);
1028 casadi_int s0 = shape.empty() ? 1 : shape[0];
1029 casadi_int s1 = shape.size() > 1 ? shape[1] : 1;
1030 output = reshape(densify(node_inputs[0]), s1, s0);
1033 }
else if (op_type ==
"Unsqueeze") {
1034 casadi_assert(!node_inputs.empty() && node_inputs[0].is_vector(),
1035 "Unsqueeze: only scalar and vector operands are supported");
1036 std::vector<casadi_int> axes;
1037 if (node_inputs.size() > 1) {
1040 for (
const auto& attr : node.attribute()) {
1041 if (attr.name() ==
"axes") axes.assign(attr.ints().begin(), attr.ints().end());
1044 casadi_assert(axes.size() == 1 && axes[0] >= -2 && axes[0] < 2,
1045 "Unsqueeze: expected one axis for a 2-D result");
1046 casadi_int axis = axes[0] < 0 ? axes[0] + 2 : axes[0];
1047 output = axis == 0 ? reshape(node_inputs[0], node_inputs[0].numel(), 1) :
1048 reshape(node_inputs[0], 1, node_inputs[0].numel());
1050 }
else if (op_type ==
"Concat") {
1054 output = horzcat(node_inputs);
1055 }
else if (axis == 1) {
1056 output = vertcat(node_inputs);
1058 casadi_error(
"Concat with axis=" + std::to_string(axis) +
1059 " not supported. Only axis=0 (vertcat) and axis=1 (horzcat) are supported.");
1062 }
else if (op_type ==
"Slice") {
1064 casadi_assert(node_inputs.size() >= 3,
1065 "Slice requires at least 3 inputs (data, starts, ends)");
1067 MX data = node_inputs[0];
1068 casadi_assert(node_inputs[1].is_constant() && node_inputs[2].is_constant(),
1069 "Slice starts and ends must be constants");
1071 DM starts_dm =
static_cast<DM>(node_inputs[1]);
1072 DM ends_dm =
static_cast<DM>(node_inputs[2]);
1075 std::vector<casadi_int> axes, steps;
1076 if (node_inputs.size() >= 4 && !node_inputs[3].is_empty()) {
1077 axes = input_ints(3);
1079 for (casadi_int k = 0; k < starts_dm.numel(); ++k) axes.push_back(k);
1081 if (node_inputs.size() >= 5 && !node_inputs[4].is_empty()) {
1082 steps = input_ints(4);
1084 steps.assign(starts_dm.numel(), 1);
1087 casadi_assert(axes.size() <= 2,
"Slice: only up to 2D slicing supported");
1088 casadi_assert(starts_dm.numel() == axes.size() && ends_dm.numel() == axes.size() &&
1089 steps.size() == axes.size(),
"Slice: starts, ends, axes and steps must have equal lengths");
1092 Slice row_slice, col_slice;
1093 casadi_int nrow = data.size1(), ncol = data.size2();
1094 for (casadi_int a = 0; a < static_cast<casadi_int>(axes.size()); ++a) {
1095 casadi_int axis = axes[a] < 0 ? axes[a] + 2 : axes[a];
1096 casadi_assert(axis == 0 || axis == 1,
"Slice: axis out of range");
1097 casadi_int n = axis == 0 ? ncol : nrow;
1098 casadi_int sp = steps[a];
1099 casadi_assert(sp != 0,
"Slice: step must not be zero");
1100 sp = std::max(-std::max(n, casadi_int(1)), std::min(sp, std::max(n, casadi_int(1))));
1101 auto bound = [&](
size_t input,
const DM& dm) -> casadi_int {
1102 auto it = integer_constants.find(node.input(input));
1103 if (it != integer_constants.end()) {
1104 int64_t v = it->second.at(a);
1106 return static_cast<casadi_int
>(std::max<int64_t>(sp > 0 ? 0 : -1,
1107 std::min<int64_t>(v, sp > 0 ? n : n - 1)));
1110 double v = dm(a).scalar();
1112 return static_cast<casadi_int
>(std::max(sp > 0 ? 0. : -1.,
1113 std::min(v,
static_cast<double>(sp > 0 ? n : n - 1))));
1115 casadi_int st = bound(1, starts_dm);
1116 casadi_int en = bound(2, ends_dm);
1118 Slice indices = (sp > 0 ? st >= en : st <= en) ? Slice(0, 0) :
1119 Slice(st, en == -1 ? std::numeric_limits<casadi_int>::max() : en, sp);
1121 col_slice = indices;
1123 row_slice = indices;
1126 output = data(row_slice, col_slice);
1128 }
else if (op_type ==
"Gather") {
1130 casadi_assert(node_inputs.size() >= 2,
"Gather requires data and indices");
1133 MX data = densify(node_inputs[0]);
1134 MX indices_mx = node_inputs[1];
1137 casadi_assert(indices_mx.is_constant(),
"Gather indices must be constant");
1138 std::vector<casadi_int> indices = input_ints(1);
1141 if (data.size2() == 1 || axis == 1) {
1142 output = data(indices, Slice());
1143 }
else if (axis == 0) {
1144 output = data(Slice(), indices);
1146 casadi_error(
"Gather: only axis 0 and 1 supported for 2D tensors");
1149 }
else if (op_type ==
"ScatterElements") {
1152 casadi_assert(node_inputs.size() >= 3,
"ScatterElements requires data, indices, updates");
1153 MX se_data = densify(node_inputs[0]);
1154 MX se_idx = node_inputs[1];
1155 MX se_upd = vec(node_inputs[2]);
1161 MX w =
MX::sym(
"scel_w", se_data.numel(), 1), g;
1162 if (se_idx.is_constant()) {
1165 w.get_nz(g,
false, vec(se_idx));
1167 output = se_data + reshape(jtimes(g, w, se_upd,
true), se_data.size1(), se_data.size2());
1170 if (se_idx.is_constant()) {
1173 output.set_nz(se_upd,
false, vec(se_idx));
1177 }
else if (op_type ==
"ScatterND") {
1181 casadi_assert(node_inputs.size() >= 3,
"ScatterND requires data, indices, updates");
1182 MX data = densify(node_inputs[0]);
1183 std::vector<casadi_int> coords =
constant_ints(node_inputs[1]);
1184 casadi_int R = data.size1();
1185 std::vector<casadi_int> pos(coords.size() / 2);
1186 for (casadi_int i = 0; i < static_cast<casadi_int>(pos.size()); ++i) {
1187 pos[i] = coords[2 * i] * R + coords[2 * i + 1];
1194 MX w =
MX::sym(
"addnz_w", data.numel(), 1), g;
1196 MX scatter = jtimes(g, w, vec(node_inputs[2]),
true);
1197 output = data + reshape(scatter, data.size1(), data.size2());
1203 }
else if (op_type ==
"Tile") {
1205 casadi_assert(node_inputs.size() >= 2,
"Tile requires data and repeats inputs");
1206 MX data = node_inputs[0];
1207 MX repeats_mx = node_inputs[1];
1208 casadi_assert(repeats_mx.is_constant(),
"Tile repeats must be constant");
1209 DM repeats_dm =
static_cast<DM>(repeats_mx);
1211 casadi_int rows_repeat = 1, cols_repeat = 1;
1212 if (repeats_dm.numel() >= 1) rows_repeat =
static_cast<casadi_int
>(repeats_dm(0).scalar());
1213 if (repeats_dm.numel() >= 2) cols_repeat =
static_cast<casadi_int
>(repeats_dm(1).scalar());
1216 output = repmat(data, cols_repeat, rows_repeat);
1218 }
else if (op_type ==
"GatherElements") {
1223 casadi_assert(node_inputs.size() >= 2,
"GatherElements requires data and indices");
1224 MX data = densify(node_inputs[0]);
1225 MX indices_mx = node_inputs[1];
1227 MX idx_vec = vec(indices_mx);
1228 MX flat = vec(data);
1229 if (indices_mx.is_constant()) {
1234 flat.get_nz(output,
false, idx_vec);
1238 output =
activation(op_type, node, node_inputs);
1241 casadi_error(
"Unsupported operation '" + op_type +
"'");
casadi_int numel() const
Get the number of elements, size1()*size2()
bool is_empty(bool both=false) const
Check if the matrix is empty, i.e. if one of the dimensions is zero.
bool is_vector() const
Check if the matrix is a row or column vector.
casadi_int size2() const
Get the second dimension (i.e. number of columns)
casadi_int size1() const
Get the first dimension (i.e. number of rows)
static MX sym(const std::string &name, casadi_int nrow=1, casadi_int ncol=1)
Create an nrow-by-ncol symbolic primitive.
static MX zeros(casadi_int nrow=1, casadi_int ncol=1)
Create a dense matrix or a matrix with specified sparsity with all entries zero.
bool verbose_
Verbose – for debugging.
casadi_int numel() const
Get shape.
static MX kron(const MX &x, const MX &b)
bool is_constant() const
Check if the node is a constant.
void get_nz(MX &m, bool ind1, const Slice &kk) const
Get a set of nonzeros (what x.nz[k] does)
Function create(const std::string &name)
Create a CasADi Function from the loaded ONNX graph.
onnx::ModelProto model_
ONNX model protocol buffer.
bool has_model_
Whether a model has been loaded.
template class CASADI_EXPORT Matrix< casadi_int >
static std::vector< casadi_int > constant_ints(const MX &m)
static bool broadcastable(const MX &a, const MX &b)
T norm_1(const std::vector< T > &x)
static MX activation(const std::string &op_type, const onnx::NodeProto &node, const std::vector< MX > &node_inputs)
double if_else(double x, double y, double z)
double sign(double x)
Sign function, note that sign(nan) == nan.
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.
static bool is_activation(const std::string &op_type)
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.
static MX elu(const MX &x, double alpha)
static void orient_vector(MX *v, const MX &o)
static bool has_unresolved_negative_axis(const onnx::GraphProto &graph)
static MX sigmoid(const MX &x)
std::string str(const T &v)
String representation, any type.
static void broadcast_pair(MX *x, MX *y)
void copy(const T1 *x, casadi_int n, T2 *y)
static bool integer_tensor(const onnx::TensorProto &tensor, std::vector< int64_t > &values)
const OpMapping * get_op_mapping_by_name(const std::string &onnx_name)
Lookup operation mapping by ONNX name (for import)
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.
T norm_2(const std::vector< T > &x)
static const onnx::NodeProto & resolve_negative_axis(const onnx::NodeProto &node, const std::map< std::string, casadi_int > &ranks, onnx::NodeProto *copy)
std::string get_string_attribute(const onnx::NodeProto &node, const std::string &name)
Read a string node attribute by name, or "" if absent.
static MX softplus(const MX &x)
static std::map< std::string, casadi_int > declared_ranks(const onnx::GraphProto &graph)
const double pi
Define pi.