Skip to content

Commit 4c666c8

Browse files
authored
[ONNX] Updated frontend's documentation (#28588)
### Details: - Updated ONNX Frontend documentation - Added list of available operators and when it was originally defined - Added script for auto-check for registration correctness and supported ops list generation ### Tickets: - 161016
1 parent 352fd39 commit 4c666c8

File tree

6 files changed

+532
-38
lines changed

6 files changed

+532
-38
lines changed

src/frontends/onnx/CMakeLists.txt

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@
55
add_subdirectory(onnx_common)
66
add_subdirectory(frontend)
77

8+
if(Python3_Interpreter_FOUND)
9+
execute_process(
10+
COMMAND ${Python3_EXECUTABLE}
11+
${CMAKE_CURRENT_SOURCE_DIR}/docs/check_supported_ops.py
12+
${CMAKE_CURRENT_SOURCE_DIR}/frontend/src/op
13+
${CMAKE_CURRENT_SOURCE_DIR}/docs/supported_ops.md
14+
RESULT_VARIABLE SCRIPT_RESULT
15+
OUTPUT_VARIABLE SCRIPT_OUTPUT
16+
ERROR_VARIABLE SCRIPT_ERROR
17+
)
18+
19+
if(NOT SCRIPT_RESULT EQUAL 0)
20+
message(FATAL_ERROR "Python script failed with return code ${SCRIPT_RESULT}\nOutput: ${SCRIPT_OUTPUT}\nError: ${SCRIPT_ERROR}")
21+
endif()
22+
endif()
23+
824
if(ENABLE_TESTS)
925
add_subdirectory(tests)
1026
endif()
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
# Copyright (C) 2018-2025 Intel Corporation
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
import os
5+
import re
6+
import sys
7+
from pathlib import Path
8+
9+
def run_in_ci():
10+
if "CI" in os.environ and os.environ["CI"].lower() == "true":
11+
return True
12+
13+
if "TF_BUILD" in os.environ and len(os.environ["TF_BUILD"]):
14+
return True
15+
16+
if "JENKINS_URL" in os.environ and len(os.environ["JENKINS_URL"]):
17+
return True
18+
19+
return False
20+
21+
22+
if not run_in_ci() and not '--manual' in sys.argv:
23+
# execute check only in CI when the code is productized
24+
exit(0)
25+
26+
if len(sys.argv) < 3:
27+
error_message = "Run in the following format: check_supported_ops.py path/to/ops supported_ops.md [--manual]\n"
28+
error_message += " --manual - script originated to run in CI, use this flag to run it manually"
29+
raise Exception(error_message)
30+
31+
lookup_path = Path(sys.argv[1])
32+
supported_ops_doc = sys.argv[2]
33+
34+
if not lookup_path.exists() or not lookup_path.is_dir():
35+
raise Exception(f"Argument \'{lookup_path}\' isn\'t a valid path to sources")
36+
37+
files = []
38+
# Looking for source files
39+
for path in lookup_path.rglob('*.cpp'):
40+
files.append(path)
41+
for path in lookup_path.rglob('*.hpp'):
42+
files.append(path)
43+
44+
# MACRO Op Name Opset Impl Domain
45+
op_regex = re.compile(r'ONNX_OP(_M)?\("([a-z0-9_]+)",\s+([^\)\}]+)[\)\}]?,\s+([a-z0-9_:]+)(,\s+[^\)]+)?\);', re.IGNORECASE)
46+
47+
ops = {}
48+
49+
known_domains = {
50+
"":"",
51+
"OPENVINO_ONNX_DOMAIN":"org.openvinotoolkit",
52+
"MICROSOFT_DOMAIN":"com.microsoft",
53+
"PYTORCH_ATEN_DOMAIN":"org.pytorch.aten",
54+
"MMDEPLOY_DOMAIN":"mmdeploy"
55+
}
56+
57+
hdr = ""
58+
with open(supported_ops_doc, 'rt') as src:
59+
table_line = 0
60+
for line in src:
61+
if table_line < 2:
62+
hdr += line
63+
if line.count('|') == 6:
64+
table_line += 1
65+
if table_line > 2:
66+
row = [cell.strip() for cell in line.split('|')] # Split line by "|" delimeter and remove spaces
67+
domain = row[1]
68+
if not domain in ops:
69+
ops[domain] = {}
70+
opname = row[2]
71+
defined = []
72+
for item in row[4].split(', '):
73+
val = 1
74+
try:
75+
val = int(item)
76+
except:
77+
continue
78+
defined.append(val)
79+
if not opname in ops[domain]:
80+
ops[domain][opname] = {'supported':[], 'defined': defined, 'limitations':row[5]}
81+
82+
documentation_errors = []
83+
84+
for file_path in files:
85+
with open(file_path.as_posix(), "r") as src:
86+
reg_macro = None
87+
for line in src:
88+
# Multiline registration
89+
if 'ONNX_OP' in line:
90+
reg_macro = ""
91+
if not reg_macro is None:
92+
reg_macro += line
93+
else:
94+
continue
95+
if not ');' in line:
96+
continue
97+
# Registration macro has been found, trying parse it
98+
m = op_regex.search(reg_macro)
99+
if m is None:
100+
documentation_errors.append(f"Registration in file {file_path.as_posix()} is corrupted {reg_macro}, please check correctness")
101+
if ');' in line: reg_macro = None
102+
continue
103+
domain = m.group(5)[2:].strip() if not m.group(5) is None else ""
104+
if not domain in known_domains:
105+
documentation_errors.append(f"Unknown domain found in file {file_path.as_posix()} with identifier {domain}, please modify check_supported_ops.py if needed")
106+
if ');' in line: reg_macro = None
107+
continue
108+
domain = known_domains[domain]
109+
opname = m.group(2)
110+
opset = m.group(3)
111+
if not domain in ops:
112+
documentation_errors.append(f"Domain {domain} is missing in a list of documented operations supported_ops.md, update it by adding operation description")
113+
if ');' in line: reg_macro = None
114+
continue
115+
if not opname in ops[domain]:
116+
documentation_errors.append(f"Operation {domain if domain=='' else domain + '.'}{opname} is missing in a list of documented operations supported_ops.md, update it by adding operation description")
117+
if ');' in line: reg_macro = None
118+
continue
119+
if opset.startswith('OPSET_SINCE'):
120+
ops[domain][opname]['supported'].append(int(opset[12:]))
121+
elif opset.startswith('OPSET_IN'):
122+
ops[domain][opname]['supported'].append(int(opset[9:]))
123+
elif opset.startswith('OPSET_RANGE'):
124+
ops[domain][opname]['supported'].append(int(opset[12:].split(',')[0]))
125+
elif opset.startswith('{'):
126+
ops[domain][opname]['supported'].append(int(opset[1:].split(',')[0]))
127+
else:
128+
documentation_errors.append(f"Domain {domain} is missing in a list of documented operations supported_ops.md, update it by adding operation description")
129+
if ');' in line: reg_macro = None
130+
continue
131+
if ');' in line:
132+
reg_macro = None
133+
134+
if len(documentation_errors) > 0:
135+
for errstr in documentation_errors:
136+
print('[ONNX Frontend] ' + errstr)
137+
raise Exception('[ONNX Frontend] failed: due to documentation errors')
138+
139+
with open(supported_ops_doc, 'wt') as dst:
140+
dst.write(hdr)
141+
for domain, ops in ops.items():
142+
for op_name in sorted(list(ops.keys())):
143+
data = ops[op_name]
144+
min_opset = data['defined'][-1] if len(data['defined']) > 0 else 1
145+
if min_opset in data['supported']:
146+
min_opset = 1
147+
dst.write(f"|{domain:<24}|{op_name:<56}|{', '.join([str(max(i, min_opset)) for i in sorted(data['supported'], reverse=True)]):<24}|{', '.join([str(i) for i in data['defined']]):<32}|{data['limitations']:<32}|\n")
148+
149+
print("Data collected and stored")

src/frontends/onnx/docs/how_to_add_op.md

Lines changed: 24 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,23 @@
33
## How to implement a new operation in ONNX FE codebase
44
ONNX operations ("op" or "ops" for short in this article) can be distinguished into two main categories: [the official ops defined in the ONNX standard](https://github.com/onnx/onnx/blob/main/docs/Operators.md) and the custom-domain ops (such as ops from the `org.openvinotoolkit`, `com.microsoft`, and `org.pytorch.aten` domains). Multiple operator handlers for different versions of an op can be defined. When importing a model, ONNX FE tries to use a handler that matches the version of the opset in the model. If such implementation doesn't exist, it will try to use the existing handler(s) starting with the greatest opset number. When adding a new operator's implementation, the implementation has to be registered using version `1` (according to an implementation requirement of ONNX FE), even if the operation has been added to the ONNX standard in an opset greater than 1.
55

6-
For example, we want to implement our new `org.openvinotoolkit.CustomAdd` operation in version `1`.
7-
The first step is to add `.cpp` and `.hpp` files in [the ops folder](../../../../src/frontends/onnx/frontend/src/op). For this particular case, it should be [op/org.openvinotoolkit](../../../../src/frontends/onnx/frontend/src/op/org.openvinotoolkit) to be consistent with the op folder layout.
8-
The declaration in `.hpp` can look like:
9-
```cpp
10-
#pragma once
6+
List of a currently available ops is available [in Supported Operations](supported_ops.md).
117

12-
#include "core/node.hpp"
8+
For example, we want to implement our new `org.openvinotoolkit.CustomAdd` operation in version `1`.
9+
The first step is to add `.cpp` file in [the ops folder](../../../../src/frontends/onnx/frontend/src/op). For this particular case, it should be [op/org.openvinotoolkit](../../../../src/frontends/onnx/frontend/src/op/org.openvinotoolkit) to be consistent with the op folder layout.
1310

14-
namespace ov {
15-
namespace frontend {
16-
namespace onnx {
17-
namespace op {
18-
namespace set_1 {
11+
The definition in `.cpp` contains an implementation of transformation from [ov::frontend::onnx::Node](../../../../src/frontends/onnx/frontend/include/onnx_import/core/node.hpp) to [ov::OutputVector](../../../../src/core/include/openvino/core/node_vector.hpp).
1912

20-
ov::OutputVector custom_add(const ov::frontend::onnx::Node& node);
13+
Transformation must be implemented in a correct namespace, which is defined as ov::frontend::onnx::domain::opset_N. Domain is operation domain name (ai_onnx for ONNX standard), opset_N - defined operation opset.
2114

22-
} // namespace set_1
23-
} // namespace op
24-
} // namespace onnx
25-
} // namespace frontend
26-
} // namespace ov
15+
The next step is to register a new op by using a macro [ONNX_OP](../../../../src/frontends/onnx/frontend/src/core/operator_set.hpp). For `org.openvinotoolkit.CustomAdd`, the registration can look like:
16+
```cpp
17+
ONNX_OP("CustomAdd", OPSET_SINCE(1), ai_onnx::opset_1::custom_add, OPENVINO_ONNX_DOMAIN);
2718
```
28-
The definition in `.cpp` contains an implementation of transformation from [ov::frontend::onnx::Node](../../../../src/frontends/onnx/frontend/include/onnx_import/core/node.hpp) to [ov::OutputVector](../../../../src/core/include/openvino/core/node_vector.hpp). Such implementation can look like:
19+
20+
Whole implementation can look like:
2921
```cpp
30-
#include "op/org.openvinotoolkit/custom_add.hpp"
22+
#include "core/operator_set.hpp"
3123
3224
#include "exceptions.hpp"
3325
#include "openvino/op/add.hpp"
@@ -36,18 +28,19 @@ The definition in `.cpp` contains an implementation of transformation from [ov::
3628
#include "openvino/op/multiply.hpp"
3729
#include "utils/common.hpp"
3830
31+
using namespace ov::op;
32+
3933
namespace ov {
4034
namespace frontend {
4135
namespace onnx {
42-
namespace op {
43-
namespace set_1 {
36+
namespace ai_onnx {
37+
namespace opset_1 {
4438
4539
ov::OutputVector custom_add(const ov::frontend::onnx::Node& node) {
40+
// CustomAdd should have at least 2 inputs
41+
common::default_op_checks(node, 2);
42+
4643
const auto& inputs = node.get_ov_inputs();
47-
CHECK_VALID_NODE(node,
48-
inputs.size() == 2,
49-
"CustomAdd should have exactly 2 inputs, got: ",
50-
inputs.size());
5144
const auto in1 = inputs[0];
5245
const auto in2 = inputs[1];
5346
const auto alpha = node.get_attribute_value<float>("alpha", 1);
@@ -64,19 +57,16 @@ ov::OutputVector custom_add(const ov::frontend::onnx::Node& node) {
6457
return {std::make_shared<v1::Multiply>(add, alpha_node)};
6558
}
6659
67-
} // namespace set_1
68-
} // namespace op
60+
ONNX_OP("CustomAdd", OPSET_SINCE(1), ai_onnx::opset_1::custom_add, OPENVINO_ONNX_DOMAIN);
61+
62+
} // namespace opset_1
63+
} // namespace ai_onnx
6964
} // namespace onnx
7065
} // namespace frontend
7166
} // namespace ov
7267
```
73-
The next step is to register a new op in [ops_bridge](../../../../src/frontends/onnx/frontend/src/ops_bridge.cpp). For `org.openvinotoolkit.CustomAdd`, the registration can look like:
74-
```cpp
75-
#include "op/org.openvinotoolkit/custom_add.hpp"
76-
...
77-
REGISTER_OPERATOR_WITH_DOMAIN(OPENVINO_ONNX_DOMAIN, "CustomAdd", 1, custom_add);
78-
```
79-
The minimum requirement to receive an approval during the code review is the implementation of [C++ unit tests](tests.md#C++-tests) for a new operation.
68+
69+
The minimum requirement to receive an approval during the code review is the implementation of [C++ unit tests](tests.md#C++-tests) for a new operation. To make a test you may need to prepare a [prototxt file](../tests/models) with a small model which implements your operation.
8070

8171

8272
## How to register a custom operation via extensions mechanism
@@ -95,7 +85,6 @@ If an OpenVINO Core operation provides exactly what you need (without decomposit
9585
```cpp
9686
core.add_extension(ov::frontend::onnx::OpExtension<ov::opset9::Add>("org.openvinotoolkit", "CustomAdd"));
9787
```
98-
If you need to register an custom operation for [OpenVINO Model Converter](../../../../tools/ovc) scenario, you should consider `SOExtension`. More details about it can be found in [Library with Extensions](../../../../docs/Extensibility_UG/Intro.md#create-a-library-with-extensions).
9988
### Python-based extensions
10089
C++ based extensions have their equivalents in Python. For `ConversionExtension`, an example of usage can look like:
10190
```python

0 commit comments

Comments
 (0)