diff --git a/docs/python/_common/onnx_sphinx.py b/docs/python/_common/onnx_sphinx.py
new file mode 100644
index 0000000000..eb45bb7383
--- /dev/null
+++ b/docs/python/_common/onnx_sphinx.py
@@ -0,0 +1,900 @@
+# pylint: disable=C0103,C0415,R0912,R0913,R0914,R0915
+"""
+Automates the generation of ONNX operators.
+"""
+import importlib
+import inspect
+import keyword
+import os
+import re
+import sys
+import textwrap
+from difflib import Differ
+
+import numpy as np
+import onnx
+from onnx.backend.test.case.base import _Exporter
+from onnx.defs import OpSchema, get_all_schemas_with_history, get_schema
+from onnx.numpy_helper import to_array
+from onnx.onnx_cpp2py_export.defs import SchemaError # pylint: disable=E1101,E0611,E0401
+from sphinx.util import logging
+
+
+def get_template(): # type: ignore
+ try:
+ from jinja2 import Template
+ except ImportError: # pragma no cover
+
+ class Template: # type: ignore
+ "Docstring template"
+
+ def __init__(self, *args):
+ pass
+
+ def render(self, **context):
+ "render"
+ schemas = context["schemas"]
+ rows = []
+ for sch in schemas:
+ doc = sch.doc or ""
+ name = sch.name
+ if name is None:
+ raise RuntimeError("An operator must have a name.")
+ rows.extend([name, "=" * len(name), "", doc, ""])
+ return "\n".join(rows)
+
+ return Template
+
+
+def _get_diff_template(): # type: ignore
+ template = get_template()
+ return template(
+ textwrap.dedent(
+ """
+
+
+
+
+ """
+ ),
+ autoescape=True,
+ )
+
+
+def _get_ops_template(): # type: ignore
+ template = get_template()
+ return template(
+ textwrap.dedent(
+ """
+ {% for sch in schemas %}
+
+ .. tag-diff-insert.
+ .. _l-onnx-op{{sch.domain.lower().replace(".", "-")}}-{{sch.name.lower()}}-{{str(sch.since_version)}}:
+
+ {{format_name_with_domain(sch)}}
+ {{'=' * len(format_name_with_domain(sch))}}
+
+ **Version**
+
+ * **name**: `{{sch.name}} (GitHub) <{{build_doc_url(sch)}}{{sch.name}}>`_
+ * **domain**: **{% if sch.domain == '' %}main{% else %}{{sch.domain}}{% endif %}**
+ * **since_version**: **{{sch.since_version}}**
+ * **function**: {{sch.has_function}}
+ * **support_level**: {{sch.support_level}}
+ * **shape inference**: {{sch.has_type_and_shape_inference_function}}
+
+ {% if sch.support_level == OpSchema.SupportType.EXPERIMENTAL %}
+ No versioning maintained for experimental ops.
+ {% else %}
+ This version of the operator has been {% if
+ sch.deprecated %}deprecated{% else %}available{% endif %}
+ **since version {{sch.since_version}}{% if
+ sch.domain %} of domain {{sch.domain}}{% endif %}**.
+ {% if len(sch.versions) > 1 %}
+ Other versions of this operator:
+ {% for v in sch.version[:-1] %} {{v}} {% endfor %}
+ {% endif %}
+ {% endif %}
+
+ **Summary**
+
+ {{process_documentation(sch.doc)}}
+ {% if sch.attributes %}
+
+ **Attributes**
+
+ {% for _, attr in sorted(sch.attributes.items())
+ %}* **{{attr.name}} - {{str(attr.type).split('.')[-1]}}**{%
+ if attr.required %} (required){% endif %} {%
+ if attr.default_value %}{{clean_default_value(attr)}}{%
+ endif %}: {{text_wrap(attr.description, 2)}}
+ {% endfor %}
+ {% endif %}
+ {% if sch.inputs %}
+
+ **Inputs**
+
+ {% if sch.min_input != sch.max_input %}Between {{sch.min_input
+ }} and {{sch.max_input}} inputs.
+ {% endif %}
+ {% for ii, inp in enumerate(sch.inputs) %}
+ * **{{getname(inp, ii)}}**{{format_option(inp)}} - **{{inp.typeStr}}**:
+ {{text_wrap(inp.description, 2)}}{% endfor %}
+ {% endif %}
+ {% if sch.outputs %}
+
+ **Outputs**
+
+ {% if sch.min_output != sch.max_output %}Between {{sch.min_output
+ }} and {{sch.max_output}} outputs.
+ {% endif %}
+ {% for ii, out in enumerate(sch.outputs) %}
+ * **{{getname(out, ii)}}**{{format_option(out)}} - **{{out.typeStr}}**:
+ {{text_wrap(out.description, 2)}}{% endfor %}
+ {% endif %}
+ {% if sch.type_constraints %}
+
+ **Type Constraints**
+
+ {% for ii, type_constraint in enumerate(sch.type_constraints)
+ %}* {{get_constraint(type_constraint, ii)}}:
+ {{text_wrap(type_constraint.description, 2)}}
+ {% endfor %}
+ {% endif %}
+ {% if get_onnx_example and is_last_schema(sch): %}
+
+ **Examples**
+
+ {% for example, code in get_onnx_example(sch.name).items(): %}
+
+ **{{ example }}**
+
+ ::
+
+ {{ format_example(code) }}
+ {% endfor %}
+ {% endif %}
+ {% endfor %}
+ """
+ ),
+ autoescape=True,
+ )
+
+
+def _get_main_template(): # type: ignore
+ template = get_template()
+ return template(
+ textwrap.dedent(
+ """
+ .. _l-onnx-operators:
+
+ {{ title }}
+ {{ "=" * len(title) }}
+
+ Lists out all the ONNX operators defined in onnxruntime.
+
+ .. toctree::
+ :hidden:
+
+ {% for p in pages %}{{ os.path.split(p)[-1] }}
+ {% endfor %}
+
+ .. tabs::
+
+ {% for t in tabs %}.. tab:: {{ t.domain_name }}
+ {{ t.render(indent=" ") }}
+ {% endfor %}
+ """
+ ),
+ autoescape=True,
+ )
+
+
+def _clean_unicode(text):
+ text = text.replace(""", '"')
+ text = text.replace("—", "-")
+ text = text.replace(" ", " ")
+ text = text.replace("'", "'")
+ text = text.replace(">", ">")
+ text = text.replace("<", "<")
+ return text
+
+
+_template_diff = _get_diff_template()
+_template_operator = _get_ops_template()
+_template_main = _get_main_template()
+__get_all_schemas_with_history = None
+
+
+_attribute_conversion_functions = {
+ onnx.AttributeProto.FLOAT: lambda att: np.float32(att.f),
+ onnx.AttributeProto.FLOATS: lambda att: [np.float32(f) for f in att.floats],
+ # AttributeProto.GRAPH(5)
+ # AttributeProto.GRAPHS(10)
+ onnx.AttributeProto.INT: lambda att: int(att.i),
+ onnx.AttributeProto.INTS: lambda att: [int(i) for i in att.ints],
+ # AttributeProto.SPARSE_TENSOR(11)
+ # AttributeProto.SPARSE_TENSORS(12)
+ onnx.AttributeProto.STRING: lambda att: att.s.decode("utf-8"),
+ onnx.AttributeProto.STRINGS: lambda att: [s.decode("utf-8") for s in att.strings],
+ onnx.AttributeProto.TENSOR: lambda att: to_array(att.t),
+ # AttributeProto.TENSORS(9)
+ # onnx.AttributeProto.TYPE_PROTO: lambda att: OnnxType(att.tp),
+ # AttributeProto.TYPE_PROTOS(14)
+}
+
+
+def _populate__get_all_schemas_with_history(): # type: ignore
+ import onnxruntime.capi.onnxruntime_pybind11_state as rtpy
+
+ get_schemas = rtpy.get_all_operator_schema or rtpy.get_all_opkernel_def
+
+ schemas = get_schemas()
+ res = {}
+ for sch in schemas:
+ domain, name = sch.domain, sch.name
+ if domain in res and name in res[domain]:
+ # already handled
+ continue
+ version = sch.since_version
+ if domain not in res:
+ res[domain] = {}
+ if name not in res[domain]:
+ res[domain][name] = {}
+ res[domain][name][version] = sch
+ return res
+
+
+def _get_all_schemas_with_history(): # type: ignore
+ global __get_all_schemas_with_history # pylint: disable=W0603
+ if __get_all_schemas_with_history is None:
+ __get_all_schemas_with_history = _populate__get_all_schemas_with_history()
+ return __get_all_schemas_with_history
+
+
+def get_domain_list(): # type: ignore
+ """
+ Returns the list of available domains.
+ """
+ return list(sorted(set(map(lambda s: s.domain, get_all_schemas_with_history()))))
+
+
+def get_operator_schemas(op_name, version=None, domain=None): # type: ignore
+ """
+ Returns all schemas mapped to an operator name.
+ :param op_name: name of the operator
+ :param version: version
+ :param domain: domain
+ :return: list of schemas
+ """
+ if version == "last" and op_name is not None:
+ if domain is not None:
+ return [get_schema(op_name, domain=domain)]
+ all_schemas = _get_all_schemas_with_history()
+ if domain is None:
+ domains = []
+ for dom, ops in all_schemas.items():
+ if op_name is None or op_name in ops:
+ domains.append(dom)
+ else:
+ domains = [domain]
+
+ # schemas
+ sch = []
+ for dom in domains:
+ ops = all_schemas[dom]
+ if op_name is None:
+ for op, v in ops.items():
+ if version is None:
+ sch.extend(v.values())
+ elif version == "last" and (dom == "" or "onnx" in dom):
+ try:
+ sch.append(get_schema(op, domain=dom))
+ except SchemaError: # pragma: no cover
+ sch.append(v[max(v)])
+ elif version == "last":
+ sch.append(v[max(v)])
+ else:
+ sch.append(v[version])
+ elif op_name in ops:
+ if version is None:
+ sch.extend(ops[op_name].values())
+ elif version in ops[op_name]:
+ sch.append(ops[op_name][version])
+
+ # sort
+ vals = [(s.domain, s.name, -s.since_version, s) for s in sch]
+ vals.sort()
+ return [v[-1] for v in vals]
+
+
+def get_rst_doc( # type: ignore
+ folder,
+ op_name=None,
+ domain=None,
+ version="last",
+ clean=True,
+ diff=False,
+ example=False,
+):
+ """
+ Returns a documentation in RST format
+ for all :class:`OnnxOperator`.
+
+ :param op_name: operator name of None for all
+ :param domain: domain
+ :param version: version, None for all, `'last'` for the most recent one
+ :param clean: clean empty lines
+ :param diff: highlights differences between two versions
+ :param example: add example to the documentation
+ :return: string
+ The function relies on module `jinja2` or replaces it
+ with a simple rendering if not present.
+ """
+ schemas = get_operator_schemas(op_name, domain=domain, version=version)
+
+ # from onnx.backend.sample.ops import collect_sample_implementations
+ # from onnx.backend.test.case import collect_snippets
+ # SNIPPETS = collect_snippets()
+ # SAMPLE_IMPLEMENTATIONS = collect_sample_implementations()
+ def format_name_with_domain(sch):
+ if version == "last":
+ if sch.domain:
+ return f"{sch.name} ({sch.domain})"
+ return sch.name
+ return f"{sch.name} - {sch.since_version}"
+
+ def format_option(obj):
+ opts = []
+ if OpSchema.FormalParameterOption.Optional == obj.option:
+ opts.append("optional")
+ elif OpSchema.FormalParameterOption.Variadic == obj.option:
+ opts.append("variadic")
+ if getattr(obj, "isHomogeneous", False):
+ opts.append("heterogeneous")
+ if opts:
+ return f" ({', '.join(opts)})"
+ return ""
+
+ def format_example(code):
+ code = textwrap.indent(code, " ")
+ return code
+
+ def get_constraint(const, ii):
+ if const.type_param_str:
+ name = const.type_param_str
+ else:
+ name = str(ii)
+ name = f"**{name}** in ("
+ if const.allowed_type_strs:
+ text = ",\n ".join(sorted(const.allowed_type_strs))
+ name += "\n " + text + "\n )"
+ return name
+
+ def getname(obj, i):
+ name = obj.name
+ if len(name) == 0:
+ return str(i)
+ return name
+
+ def process_documentation(doc):
+ if doc is None:
+ doc = ""
+ if not isinstance(doc, str):
+ raise TypeError(f"doc must be a string not {type(doc)!r} - {doc + 42!r}.") # pragma: no cover
+ doc = textwrap.dedent(doc)
+ main_docs_url = "https://github.com/onnx/onnx/blob/master/"
+ rep = {
+ "[the doc](IR.md)": "`ONNX <{0}docs/IR.md>`_",
+ "[the doc](Broadcasting.md)": "`Broadcasting in ONNX <{0}docs/Broadcasting.md>`_",
+ "": "",
+ "
": "",
+ "": "* ",
+ "": " ",
+ "": "",
+ "": "",
+ "": "``",
+ "": "``",
+ "
": "\n",
+ }
+ for k, v in rep.items():
+ doc = doc.replace(k, v.format(main_docs_url))
+ move = 0
+ lines = []
+ for line in doc.split("\n"):
+ if line.startswith("```"):
+ if move > 0:
+ move -= 4
+ lines.append("\n")
+ else:
+ lines.append("::\n")
+ move += 4
+ elif move > 0:
+ lines.append(" " * move + line)
+ else:
+ lines.append(line)
+ return "\n".join(lines)
+
+ def build_doc_url(sch):
+ doc_url = "https://github.com/onnx/onnx/blob/main/docs/Operators"
+ if "ml" in sch.domain:
+ doc_url += "-ml"
+ doc_url += ".md"
+ doc_url += "#"
+ if sch.domain not in (None, "", "ai.onnx"):
+ doc_url += sch.domain + "."
+ return doc_url
+
+ def format_default_value(value):
+ if isinstance(value, float):
+ formatted = str(np.round(value, 5))
+ # use default formatting, unless too long.
+ if len(formatted) > 10:
+ formatted = f"({value:e})"
+ return formatted
+ if isinstance(value, (bytes, bytearray)):
+ return value.decode("utf-8")
+ return str(value)
+
+ def clean_default_value(attr):
+ if isinstance(attr.default_value, str):
+ raise TypeError(f"Unexpected type for {type(attr)} - {attr}.")
+ if not attr.default_value.name:
+ return ""
+ default_value = onnx.helper.get_attribute_value(attr.default_value)
+ if isinstance(default_value, onnx.AttributeProto) and hasattr(default_value, "default_value"):
+ if attr.type in _attribute_conversion_functions:
+ sval = _attribute_conversion_functions[attr.type](default_value)
+ return f"(default is ``{sval!r}``)"
+
+ if isinstance(default_value, list):
+ sval = [format_default_value(val) for val in default_value]
+ else:
+ sval = format_default_value(default_value)
+ return f"(default is ``{sval!r}``)"
+
+ def text_wrap(text, indent):
+ s = " " * indent
+ lines = textwrap.wrap(text, initial_indent=s, subsequent_indent=s)
+ return "\n".join(lines)
+
+ fnwd = format_name_with_domain
+ tmpl = _template_operator
+ docs = tmpl.render(
+ schemas=schemas,
+ OpSchema=OpSchema,
+ len=len,
+ getattr=getattr,
+ sorted=sorted,
+ format_option=format_option,
+ get_constraint=get_constraint,
+ getname=getname,
+ enumerate=enumerate,
+ format_name_with_domain=fnwd,
+ process_documentation=process_documentation,
+ build_doc_url=build_doc_url,
+ text_wrap=text_wrap,
+ str=str,
+ clean_default_value=clean_default_value,
+ get_onnx_example=get_onnx_example if example else None,
+ format_example=format_example,
+ is_last_schema=is_last_schema,
+ )
+ docs = _clean_unicode(docs)
+
+ d_links = {}
+ for schema in schemas:
+ sdom = schema.domain.replace(".", "-")
+ d_links[schema.since_version] = f"l-onnx-op{sdom}-{schema.name.lower()}-{schema.since_version}"
+
+ if diff:
+ lines = docs.split("\n")
+ new_lines = [""]
+ for line_ in lines:
+ line = line_.rstrip("\r\t ")
+ if len(line) == 0 and len(new_lines[-1]) == 0:
+ continue
+ new_lines.append(line)
+ docs = "\n".join(new_lines)
+ docs, d_links_diff = _insert_diff(
+ folder,
+ docs,
+ ".. tag-diff-insert.",
+ op_name=op_name,
+ version=version,
+ domain=domain,
+ )
+ d_links.update(d_links_diff)
+
+ if clean:
+ lines = docs.split("\n")
+ new_lines = [""]
+ for line_ in lines:
+ line = line_.rstrip("\r\t ")
+ if len(line) == 0 and len(new_lines[-1]) == 0:
+ continue
+ new_lines.append(line)
+ docs = "\n".join(new_lines)
+
+ return docs, d_links
+
+
+def _insert_diff(folder, docs, split=".. tag-diff-insert.", op_name=None, version=None, domain=None): # type: ignore
+ """
+ Splits a using `split`, insert HTML differences between pieces.
+ The function relies on package `pyquickhelper`.
+ """
+ spl = docs.split(split)
+ if len(spl) <= 1:
+ return docs
+
+ reg = re.compile("([A-Z][A-Za-z0-9_]*) - ([0-9]+)")
+
+ d_links = {} # type: ignore
+ pieces = [spl[0]] # type: ignore
+ mds = [] # type: ignore
+ for i in range(1, len(spl)):
+ spl1 = spl[i - 1].strip("\n ")
+ spl2 = spl[i].strip("\n ")
+ vers1 = reg.findall(spl1)
+ vers2 = reg.findall(spl2)
+
+ spl1 = spl1.split("**Examples**")[0].replace("`", "")
+ spl2 = spl2.split("**Examples**")[0].replace("`", "")
+ spl1 = spl1.split("**Summary**")[-1].strip("\n ")
+ spl2 = spl2.split("**Summary**")[-1].strip("\n ")
+ if len(spl1) < 5 or len(spl2) < 5:
+ pieces.append(spl[i])
+ continue
+ if len(vers1) == 0:
+ raise ValueError(f"Unable to find version {version!r} in\n{spl1}")
+ if len(vers2) == 0:
+ raise ValueError(f"Unable to find version {version!r} in\n{spl2}")
+ v2 = vers2[0][1]
+ v1 = vers1[0][1]
+
+ if len(mds) == 0:
+ mds.append((v1, textwrap.dedent(spl1.strip(" \n\r\t")).splitlines(keepends=True)))
+ mds.append((v2, textwrap.dedent(spl2.strip(" \n\r\t")).splitlines(keepends=True)))
+
+ if len(mds) > 1:
+ pieces.extend([".. toctree::", ""])
+
+ for di in range(len(mds) - 1):
+ dj = len(mds) - 1
+
+ v1, s1 = mds[di]
+ v2, s2 = mds[dj]
+ d = Differ()
+ result = list(d.compare(s2, s1))
+ raw = "".join(result)
+
+ tmpl = _template_diff
+ diff = tmpl.render(
+ op_name=op_name,
+ version1=v2,
+ version2=v1,
+ div_name=f"div_{op_name}_{i}",
+ diff_content=raw,
+ )
+ diff = _clean_unicode(diff)
+
+ title = f"{op_name} - {v2} vs {v1}"
+
+ name = f"text_diff_{op_name}_{v2}_{v1}"
+ sdom = domain.replace(".", "-")
+ link = f"l-onnx-op{sdom}-{op_name.lower()}-d{v2}-{v1}"
+ d_links[int(v2), int(v1)] = link
+ content = "\n".join(
+ [
+ "",
+ f".. _{link}:",
+ "",
+ title,
+ "=" * len(title),
+ "",
+ "Next section compares an older to a newer version of the same operator ",
+ "after both definition are converted into markdown text.",
+ "Green means an addition to the newer version, red means a deletion.",
+ "Anything else is unchanged.",
+ "",
+ ".. raw:: html",
+ "",
+ textwrap.indent(diff, " "),
+ ]
+ )
+ filename = os.path.join(folder, name + ".rst")
+ if os.path.exists(filename):
+ with open(filename, encoding="utf-8") as f:
+ old_content = f.read()
+ write = old_content != content
+ else:
+ write = True
+ if write:
+ with open(filename, "w", encoding="utf-8") as f:
+ f.write(content)
+ pieces.append(f" {name}")
+
+ pieces.extend(["", spl[i]])
+
+ return "\n".join(pieces), d_links
+
+
+def change_style(name: str) -> str:
+ """
+ Switches from *AaBb* into *aa_bb*.
+ :param name: name to convert
+ :return: converted name
+ """
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", name)
+ s2 = re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
+ return s2 if not keyword.iskeyword(s2) else s2 + "_"
+
+
+def _process_example(code: str) -> str:
+ """
+ Add necessary imports to make the example work.
+ """
+ code = code.replace(" # type: ignore", "")
+ missing_imports = ["import numpy as np", "import onnx"]
+ elements = [*missing_imports, "", "", code.strip("\n"), ""]
+ return "\n".join(elements)
+
+
+def get_onnx_example(op_name): # type: ignore
+ """
+ Retrieves examples associated to one operator
+ stored in onnx packages.
+ :param op_name: operator name
+ :param fmt: rendering format
+ :return: dictionary
+ """
+ modules = [
+ f"onnx.backend.test.case.node.{op_name.lower()}",
+ f"onnx.backend.test.case.node.{change_style(op_name).lower()}",
+ ]
+ module = None
+ for m in modules:
+ try:
+ mod = importlib.import_module(m)
+ module = m
+ except ImportError:
+ continue
+ if module is None:
+ # Unable to find an example for 'op_name'.
+ return {}
+ results = {} # type: ignore
+ for v in mod.__dict__.values():
+ if not isinstance(v, _Exporter):
+ continue
+ code_cls = inspect.getsource(v)
+ codes = code_cls.split("@staticmethod")
+ for me in v.__dict__:
+ if not me.startswith("export"):
+ continue
+ sub = f" {me}()"
+ found = None
+ for code in codes:
+ if sub in code:
+ found = code
+ if found is None:
+ raise RuntimeError(f"Unable to find {sub!r} in\n{code_cls}") # pragma: no cover
+ found = textwrap.dedent(found)
+ lines = found.split("\n")
+ first = 0
+ for i in range(len(lines)): # pylint: disable=C0200
+ if lines[i].startswith("def "):
+ first = i + 1
+ found = textwrap.dedent("\n".join(lines[first:]))
+ key = me[len("export") :]
+ if key == "":
+ key = "default"
+ if key in results:
+ key = f"example {len(results) + 1}"
+ results[key] = _process_example(found)
+ return results
+
+
+def is_last_schema(sch: OpSchema) -> bool:
+ """
+ Tells if this is the most recent schema for this operator.
+ :param sch: schema
+ :return: True
+ """
+ try:
+ last = get_schema(sch.name, domain=sch.domain)
+ except SchemaError: # pragma: no cover
+ # raise RuntimeError(
+ # "Unable to find schema for operator %r and domain %r."
+ # "" % (sch.name, sch.domain))
+ return True
+ return last.since_version == sch.since_version
+
+
+def onnx_documentation_folder(
+ folder, title="ONNX Operators in onnxruntime", flog=None, max_opsets=None
+): # type: ignore
+ """
+ Creates documentation in a folder for all known
+ ONNX operators defined in onnxruntime or a subset.
+ :param folder: folder where to write the documentation
+ :param title: index title
+ :param flog: logging function
+ :param max_opsets: included operator definition up to this opsets
+ :return: list of creates files
+ """
+
+ class _Table:
+ def __init__(self, ops, domain, title=None):
+ self.title = title or domain
+ self.domain = domain
+ self.ops = ops
+
+ @property
+ def domain_name(self):
+ title = self.domain
+ if title == "":
+ title = "ai.onnx"
+ return title
+
+ def render(self, indent=""):
+ table_dom = [""]
+ table_dom.extend(
+ [
+ ".. list-table::",
+ " :widths: 10 10 10",
+ " :header-rows: 1",
+ "",
+ " * - operator",
+ " - versions",
+ " - differences",
+ ]
+ )
+
+ for op in self.ops:
+ name = op["name"]
+ dom = self.domain.replace(".", "-")
+ table_dom.append(f" * - :ref:`l-onnx-doc{dom}-{name}`")
+ versions = list(reversed(sorted((k, v) for k, v in op["links"].items() if isinstance(k, int))))
+ col1 = ", ".join(f":ref:`{k} <{v}>`" for k, v in versions)
+ diffs = list(reversed(sorted((k, v) for k, v in op["links"].items() if isinstance(k, tuple))))
+ col2 = ", ".join(f":ref:`{k[1]}/{k[0]} <{v}>`" for k, v in diffs)
+ table_dom.append(f" - {col1}")
+ table_dom.append(f" - {col2}")
+ table_dom.append("")
+ if indent != "":
+ for i in range(len(table_dom)): # pylint: disable=C0200
+ table_dom[i] = indent + table_dom[i]
+ res = "\n".join(table_dom)
+ return res
+
+ all_schemas_available = _get_all_schemas_with_history()
+
+ # filter out operator under development
+ all_schemas = {}
+ for domain, ops in all_schemas_available.items():
+ max_version = None if max_opsets is None else max_opsets.get(domain, None)
+ d = {}
+ for op, schemas in ops.items():
+ vers = {}
+ for version, schema in schemas.items():
+ if max_version is not None and version > max_version:
+ continue
+ vers[version] = schema
+ d[op] = vers
+ all_schemas[domain] = d
+
+ if not os.path.exists(folder):
+ os.makedirs(folder)
+
+ pages = []
+ tables = []
+
+ # loop on domains
+ for dom in sorted(all_schemas):
+ sdom = "ai.onnx" if dom == "" else dom
+ dom_pages = []
+
+ do = all_schemas[dom]
+ if len(do) == 0:
+ continue
+
+ # loop on operators
+ for op in sorted(do):
+ if flog is not None:
+ flog(f"generate page for onnx {dom!r} - {op!r}") # pragma: no cover
+ page_name = f"onnx_{dom.replace('.', '')}_{op}"
+ doc, d_links = get_rst_doc(folder, op, domain=dom, version=None, example=True, diff=True)
+ if dom == "":
+ main = op
+ else:
+ main = f"{dom} - {op}"
+ sdom = dom.replace(".", "-")
+ ref_link = f".. _l-onnx-doc{sdom}-{op}:"
+ rows = [
+ "",
+ ref_link,
+ "",
+ "=" * len(main),
+ main,
+ "=" * len(main),
+ "",
+ doc,
+ ]
+
+ full = os.path.join(folder, page_name + ".rst")
+ content = "\n".join(rows)
+ if os.path.exists(full):
+ with open(full, encoding="utf-8") as f:
+ old_content = f.read()
+ write = old_content != content
+ else:
+ write = True
+ if write:
+ with open(full, "w", encoding="utf-8") as f:
+ f.write(content)
+ pages.append(full)
+ dom_pages.append({"name": op, "links": d_links})
+
+ tables.append(_Table(dom_pages, dom, sdom))
+
+ # final
+ tmpl = _template_main
+ index = tmpl.render(pages=pages, tabs=tables, os=os, len=len, title=title)
+ index = _clean_unicode(index)
+ page_name = os.path.join(folder, "index.rst")
+ with open(page_name, "w", encoding="utf-8") as f:
+ f.write(index)
+ pages.append(page_name)
+ return pages
+
+
+def _generate_op_doc(app):
+ logger = logging.getLogger(__name__)
+ folder = app.config.onnx_doc_folder
+ max_opsets = app.config.max_opsets
+ onnx_documentation_folder(folder, flog=logger.info, max_opsets=max_opsets)
+
+
+def setup(app):
+ """
+ Sphinx extension `onnx_sphinx` displays documentation
+ on ONN Operators.
+ """
+ import sphinx
+
+ app.add_config_value("onnx_doc_folder", "operators", "env")
+ app.add_config_value("max_opsets", {}, "env")
+ app.connect("builder-inited", _generate_op_doc)
+ return {"version": sphinx.__display_version__, "parallel_read_safe": True}
+
+
+if "debug" in sys.argv:
+ print("DEBUG")
+ onnx_documentation_folder("_debug", flog=print)
+ print("END")
diff --git a/docs/python/inference/api_summary.rst b/docs/python/inference/api_summary.rst
index 8e0d6ab57e..cecd62aff1 100644
--- a/docs/python/inference/api_summary.rst
+++ b/docs/python/inference/api_summary.rst
@@ -3,9 +3,6 @@
API
===
-.. contents::
- :local:
-
API Overview
============
@@ -36,8 +33,9 @@ the kernel is executed on CPU.
.. code-block:: python
- session = onnxruntime.InferenceSession(model,
- providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
+ session = onnxruntime.InferenceSession(
+ model, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']
+ )
The list of available execution providers can be found here: `Execution Providers `_.
@@ -53,7 +51,11 @@ profiling on the session:
options = onnxruntime.SessionOptions()
options.enable_profiling=True
- session = onnxruntime.InferenceSession('model.onnx', sess_options=options, providers=['CUDAExecutionProvider', 'CPUExecutionProvider']))
+ session = onnxruntime.InferenceSession(
+ 'model.onnx',
+ sess_options=options,
+ providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
+ )
Data inputs and outputs
@@ -78,7 +80,10 @@ numpy arrays.
np.array_equal(ortvalue.numpy(), X) # 'True'
# ortvalue can be provided as part of the input feed to a model
- session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']))
+ session = onnxruntime.InferenceSession(
+ 'model.onnx',
+ providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
+ )
results = session.run(["Y"], {"X": ortvalue})
By default, *ONNX Runtime* always places input(s) and output(s) on CPU. Having the data on CPU
@@ -101,7 +106,10 @@ use IOBinding to copy the data onto the GPU.
.. code-block:: python
# X is numpy array on cpu
- session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']))
+ session = onnxruntime.InferenceSession(
+ 'model.onnx',
+ providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
+ )
io_binding = session.io_binding()
# OnnxRuntime will copy the data over to the CUDA device if 'input' is consumed by nodes on the CUDA device
io_binding.bind_cpu_input('input', X)
@@ -115,7 +123,10 @@ The input data is on a device, users directly use the input. The output data is
# X is numpy array on cpu
X_ortvalue = onnxruntime.OrtValue.ortvalue_from_numpy(X, 'cuda', 0)
- session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']))
+ session = onnxruntime.InferenceSession(
+ 'model.onnx',
+ providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
+ )
io_binding = session.io_binding()
io_binding.bind_input(name='input', device_type=X_ortvalue.device_name(), device_id=0, element_type=np.float32, shape=X_ortvalue.shape(), buffer_ptr=X_ortvalue.data_ptr())
io_binding.bind_output('output')
@@ -129,10 +140,27 @@ The input data and output data are both on a device, users directly use the inpu
#X is numpy array on cpu
X_ortvalue = onnxruntime.OrtValue.ortvalue_from_numpy(X, 'cuda', 0)
Y_ortvalue = onnxruntime.OrtValue.ortvalue_from_shape_and_type([3, 2], np.float32, 'cuda', 0) # Change the shape to the actual shape of the output being bound
- session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']))
+ session = onnxruntime.InferenceSession(
+ 'model.onnx',
+ providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
+ )
io_binding = session.io_binding()
- io_binding.bind_input(name='input', device_type=X_ortvalue.device_name(), device_id=0, element_type=np.float32, shape=X_ortvalue.shape(), buffer_ptr=X_ortvalue.data_ptr())
- io_binding.bind_output(name='output', device_type=Y_ortvalue.device_name(), device_id=0, element_type=np.float32, shape=Y_ortvalue.shape(), buffer_ptr=Y_ortvalue.data_ptr())
+ io_binding.bind_input(
+ name='input',
+ device_type=X_ortvalue.device_name(),
+ device_id=0,
+ element_type=np.float32,
+ shape=X_ortvalue.shape(),
+ buffer_ptr=X_ortvalue.data_ptr()
+ )
+ io_binding.bind_output(
+ name='output',
+ device_type=Y_ortvalue.device_name(),
+ device_id=0,
+ element_type=np.float32,
+ shape=Y_ortvalue.shape(),
+ buffer_ptr=Y_ortvalue.data_ptr()
+ )
session.run_with_iobinding(io_binding)
@@ -144,9 +172,19 @@ Users can thus consume the *ONNX Runtime* allocated memory for the output as an
#X is numpy array on cpu
X_ortvalue = onnxruntime.OrtValue.ortvalue_from_numpy(X, 'cuda', 0)
- session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']))
+ session = onnxruntime.InferenceSession(
+ 'model.onnx',
+ providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
+ )
io_binding = session.io_binding()
- io_binding.bind_input(name='input', device_type=X_ortvalue.device_name(), device_id=0, element_type=np.float32, shape=X_ortvalue.shape(), buffer_ptr=X_ortvalue.data_ptr())
+ io_binding.bind_input(
+ name='input',
+ device_type=X_ortvalue.device_name(),
+ device_id=0,
+ element_type=np.float32,
+ shape=X_ortvalue.shape(),
+ buffer_ptr=X_ortvalue.data_ptr()
+ )
#Request ONNX Runtime to bind and allocate memory on CUDA for 'output'
io_binding.bind_output('output', 'cuda')
session.run_with_iobinding(io_binding)
@@ -164,7 +202,10 @@ Users can bind *OrtValue* (s) directly.
#X is numpy array on cpu
X_ortvalue = onnxruntime.OrtValue.ortvalue_from_numpy(X, 'cuda', 0)
Y_ortvalue = onnxruntime.OrtValue.ortvalue_from_shape_and_type([3, 2], np.float32, 'cuda', 0) # Change the shape to the actual shape of the output being bound
- session = onnxruntime.InferenceSession('model.onnx', providers=['CUDAExecutionProvider', 'CPUExecutionProvider']))
+ session = onnxruntime.InferenceSession(
+ 'model.onnx',
+ providers=['CUDAExecutionProvider', 'CPUExecutionProvider'])
+ )
io_binding = session.io_binding()
io_binding.bind_ortvalue_input('input', X_ortvalue)
io_binding.bind_ortvalue_output('output', Y_ortvalue)
diff --git a/docs/python/inference/conf.py b/docs/python/inference/conf.py
index 5febf5ef5a..bca894c31d 100644
--- a/docs/python/inference/conf.py
+++ b/docs/python/inference/conf.py
@@ -1,5 +1,6 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+# pylint: disable=C0103
# -*- coding: utf-8 -*-
#
@@ -7,15 +8,17 @@
import os
import shutil # noqa: F401
-import sys # noqa: F401
+import sys
import onnxruntime
+sys.path.append(os.path.join(os.path.dirname(__file__), "..", "_common"))
+
# import recommonmark
# -- Project information -----------------------------------------------------
-project = "ONNX Runtime"
+project = "Python API"
copyright = "2018-2023, Microsoft"
author = "Microsoft"
version = onnxruntime.__version__
@@ -35,6 +38,9 @@ extensions = [
"sphinx.ext.graphviz",
"pyquickhelper.sphinxext.sphinx_runpython_extension",
"sphinxcontrib.googleanalytics",
+ "sphinx_exec_code",
+ "sphinx_tabs.tabs",
+ "onnx_sphinx",
]
templates_path = ["_templates"]
@@ -50,14 +56,43 @@ language = "en"
exclude_patterns = []
pygments_style = "default"
autoclass_content = "both"
+master_doc = "index"
+onnx_doc_folder = os.path.join(os.path.abspath(os.path.dirname(__file__)), "operators")
+pygments_style = "sphinx"
# -- Options for HTML output -------------------------------------------------
-html_theme = "alabaster"
html_logo = "ONNX_Runtime_icon.png"
html_static_path = ["_static"]
+html_theme = "furo"
graphviz_output_format = "svg"
+html_context = {
+ "default_mode": "auto", # auto: the documentation theme will follow the system default that you have set (light or dark)
+}
+
+html_theme_options = {
+ "collapse_navigation": True,
+ "external_links": [
+ {"name": "onnxruntime", "url": "https://onnxruntime.ai/"},
+ {"name": "github", "url": "https://github.com/microsoft/onnxruntime"},
+ ],
+ "github_url": "https://github.com/microsoft/onnxruntime",
+ "navbar_center": [],
+ "navigation_depth": 5,
+ "page_sidebar_items": [], # default setting is: ["page-toc", "edit-this-page", "sourcelink"],
+ "show_nav_level": 0,
+ "show_prev_next": True,
+ "show_toc_level": 0,
+ # needed for sphinx 6.0
+ "logo": {
+ "text": project,
+ "image_light": html_logo,
+ "image_dark": html_logo,
+ "alt_text": project,
+ },
+}
+
# -- Options for Google Analytics -------------------------------------------------
googleanalytics_id = "UA-156955408-1"
diff --git a/docs/python/inference/examples/plot_convert_pipeline_vectorizer.py b/docs/python/inference/examples/plot_convert_pipeline_vectorizer.py
new file mode 100644
index 0000000000..06e9e8d29e
--- /dev/null
+++ b/docs/python/inference/examples/plot_convert_pipeline_vectorizer.py
@@ -0,0 +1,97 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
+"""
+Train, convert and predict with ONNX Runtime
+============================================
+
+This example demonstrates an end to end scenario
+starting with the training of a scikit-learn pipeline
+which takes as inputs not a regular vector but a
+dictionary ``{ int: float }`` as its first step is a
+`DictVectorizer `_.
+
+Train a pipeline
+++++++++++++++++
+
+The first step consists in creating a dummy datasets.
+"""
+import pandas
+from sklearn.datasets import make_regression
+from sklearn.model_selection import train_test_split
+
+X, y = make_regression(1000, n_targets=1)
+
+X_train, X_test, y_train, y_test = train_test_split(X, y)
+X_train_dict = pandas.DataFrame(X_train[:, 1:]).T.to_dict().values()
+X_test_dict = pandas.DataFrame(X_test[:, 1:]).T.to_dict().values()
+
+####################################
+# We create a pipeline.
+
+from sklearn.ensemble import GradientBoostingRegressor # noqa: E402
+from sklearn.feature_extraction import DictVectorizer # noqa: E402
+from sklearn.pipeline import make_pipeline # noqa: E402
+
+pipe = make_pipeline(DictVectorizer(sparse=False), GradientBoostingRegressor())
+
+pipe.fit(X_train_dict, y_train)
+
+####################################
+# We compute the prediction on the test set
+# and we show the confusion matrix.
+from sklearn.metrics import r2_score # noqa: E402
+
+pred = pipe.predict(X_test_dict)
+print(r2_score(y_test, pred))
+
+####################################
+# Conversion to ONNX format
+# +++++++++++++++++++++++++
+#
+# We use module
+# `sklearn-onnx `_
+# to convert the model into ONNX format.
+
+from skl2onnx import convert_sklearn # noqa: E402
+from skl2onnx.common.data_types import DictionaryType, FloatTensorType, Int64TensorType # noqa: E402
+
+# initial_type = [('float_input', DictionaryType(Int64TensorType([1]), FloatTensorType([])))]
+initial_type = [("float_input", DictionaryType(Int64TensorType([1]), FloatTensorType([])))]
+onx = convert_sklearn(pipe, initial_types=initial_type, target_opset=17)
+with open("pipeline_vectorize.onnx", "wb") as f:
+ f.write(onx.SerializeToString())
+
+##################################
+# We load the model with ONNX Runtime and look at
+# its input and output.
+import onnxruntime as rt # noqa: E402
+from onnxruntime.capi.onnxruntime_pybind11_state import InvalidArgument # noqa: E402
+
+sess = rt.InferenceSession("pipeline_vectorize.onnx", providers=rt.get_available_providers())
+
+inp, out = sess.get_inputs()[0], sess.get_outputs()[0]
+print(f"input name='{inp.name}' and shape={inp.shape} and type={inp.type}")
+print(f"output name='{out.name}' and shape={out.shape} and type={out.type}")
+
+##################################
+# We compute the predictions.
+# We could do that in one call:
+
+try:
+ sess.run([out.name], {inp.name: X_test_dict})[0]
+except (RuntimeError, InvalidArgument) as e:
+ print(e)
+
+#############################
+# But it fails because, in case of a DictVectorizer,
+# ONNX Runtime expects one observation at a time.
+pred_onx = [sess.run([out.name], {inp.name: row})[0][0, 0] for row in X_test_dict]
+
+###############################
+# We compare them to the model's ones.
+print(r2_score(pred, pred_onx))
+
+#########################
+# Very similar. *ONNX Runtime* uses floats instead of doubles,
+# that explains the small discrepencies.
diff --git a/docs/python/inference/examples/plot_pipeline.py b/docs/python/inference/examples/plot_pipeline.py
index 7e632f0d6a..e0bae57b7e 100644
--- a/docs/python/inference/examples/plot_pipeline.py
+++ b/docs/python/inference/examples/plot_pipeline.py
@@ -11,9 +11,6 @@ in ONNX format than looking into its node with
how to draw a model and to retrieve it in *json*
format.
-.. contents::
- :local:
-
Retrieve a model in JSON format
+++++++++++++++++++++++++++++++
diff --git a/docs/python/inference/examples/plot_train_convert_predict.py b/docs/python/inference/examples/plot_train_convert_predict.py
index bc6ca0c18d..dcbc84b207 100644
--- a/docs/python/inference/examples/plot_train_convert_predict.py
+++ b/docs/python/inference/examples/plot_train_convert_predict.py
@@ -1,9 +1,10 @@
# Copyright (c) Microsoft Corporation. All rights reserved.
# Licensed under the MIT License.
+# pylint: disable=C0411,C0412,C0413
"""
-.. _l-logreg-example:
+.. _l-logreg-example-speed:
Train, convert and predict with ONNX Runtime
============================================
@@ -12,9 +13,6 @@ This example demonstrates an end to end scenario
starting with the training of a machine learned model
to its use in its converted from.
-.. contents::
- :local:
-
Train a logistic regression
+++++++++++++++++++++++++++
@@ -22,19 +20,16 @@ The first step consists in retrieving the iris datset.
"""
from sklearn.datasets import load_iris
+from sklearn.linear_model import LogisticRegression
+from sklearn.model_selection import train_test_split
iris = load_iris()
X, y = iris.data, iris.target
-
-from sklearn.model_selection import train_test_split # noqa: E402
-
X_train, X_test, y_train, y_test = train_test_split(X, y)
####################################
# Then we fit a model.
-from sklearn.linear_model import LogisticRegression # noqa: E402
-
clr = LogisticRegression()
clr.fit(X_train, y_train)
@@ -114,7 +109,7 @@ pprint.pprint(prob_rt[0:3])
from timeit import Timer # noqa: E402
-def speed(inst, number=10, repeat=20):
+def speed(inst, number=5, repeat=10):
timer = Timer(inst, globals=globals())
raw = numpy.array(timer.repeat(repeat, number=number))
ave = raw.sum() / len(raw) / number
@@ -145,7 +140,7 @@ def loop(X_test, fct, n=None):
print("Execution time for clr.predict")
-speed("loop(X_test, clr.predict, 100)")
+speed("loop(X_test, clr.predict, 50)")
def sess_predict(x):
@@ -153,13 +148,13 @@ def sess_predict(x):
print("Execution time for sess_predict")
-speed("loop(X_test, sess_predict, 100)")
+speed("loop(X_test, sess_predict, 50)")
#####################################
# Let's do the same for the probabilities.
print("Execution time for predict_proba")
-speed("loop(X_test, clr.predict_proba, 100)")
+speed("loop(X_test, clr.predict_proba, 50)")
def sess_predict_proba(x):
@@ -167,7 +162,7 @@ def sess_predict_proba(x):
print("Execution time for sess_predict_proba")
-speed("loop(X_test, sess_predict_proba, 100)")
+speed("loop(X_test, sess_predict_proba, 50)")
#####################################
# This second comparison is better as
@@ -182,7 +177,7 @@ speed("loop(X_test, sess_predict_proba, 100)")
# We first train and save a model in ONNX format.
from sklearn.ensemble import RandomForestClassifier # noqa: E402
-rf = RandomForestClassifier()
+rf = RandomForestClassifier(n_estimators=10)
rf.fit(X_train, y_train)
initial_type = [("float_input", FloatTensorType([1, 4]))]
@@ -201,10 +196,10 @@ def sess_predict_proba_rf(x):
print("Execution time for predict_proba")
-speed("loop(X_test, rf.predict_proba, 100)")
+speed("loop(X_test, rf.predict_proba, 50)")
print("Execution time for sess_predict_proba")
-speed("loop(X_test, sess_predict_proba_rf, 100)")
+speed("loop(X_test, sess_predict_proba_rf, 50)")
##################################
# Let's see with different number of trees.
@@ -224,8 +219,8 @@ for n_trees in range(5, 51, 5):
def sess_predict_proba_loop(x):
return sess.run([prob_name], {input_name: x.astype(numpy.float32)})[0] # noqa: B023
- tsk = speed("loop(X_test, rf.predict_proba, 100)", number=5, repeat=5)
- trt = speed("loop(X_test, sess_predict_proba_loop, 100)", number=5, repeat=5)
+ tsk = speed("loop(X_test, rf.predict_proba, 25)", number=5, repeat=4)
+ trt = speed("loop(X_test, sess_predict_proba_loop, 25)", number=5, repeat=4)
measures.append({"n_trees": n_trees, "sklearn": tsk, "rt": trt})
from pandas import DataFrame # noqa: E402
diff --git a/docs/python/inference/index.rst b/docs/python/inference/index.rst
index 06816409ab..c99edda46a 100644
--- a/docs/python/inference/index.rst
+++ b/docs/python/inference/index.rst
@@ -1,9 +1,10 @@
-Python Bindings for ONNX Runtime
-================================
+Python API
+==========
ONNX Runtime is a performance-focused scoring engine for Open Neural Network Exchange (ONNX) models.
-For more information on ONNX Runtime, please see `aka.ms/onnxruntime `_ or the `Github project `_.
+For more information on ONNX Runtime, please see `aka.ms/onnxruntime `_
+or the `Github project `_.
.. toctree::
:maxdepth: 1
@@ -11,3 +12,4 @@ For more information on ONNX Runtime, please see `aka.ms/onnxruntime