.. _l-design-litert-converter:
===================================
LiteRT / TFLite Export to ONNX
===================================
.. toctree::
:maxdepth: 1
supported_ops
:func:`yobx.litert.to_onnx` converts a :epkg:`TFLite`/:epkg:`LiteRT`
``.tflite`` model into an :class:`onnx.ModelProto`. The implementation is
a **proof-of-concept** that parses the binary
`FlatBuffer `_ format of ``.tflite`` files with a
minimal pure-Python parser (no external library required) and converts each
operator in the graph to its ONNX equivalent via a registry of op-level
converters.
High-level workflow
===================
.. code-block:: text
.tflite file / bytes
│
▼
to_onnx() ← parse_tflite_model() + build TFLiteModel
│
▼
TFLiteModel (pure-Python object hierarchy)
│
▼
_convert_subgraph()
│ for every operator in the subgraph …
▼
op converter ← emits ONNX node(s) via GraphBuilder.op.*
│
▼
GraphBuilder.to_onnx() ← validates and returns ModelProto
The steps in detail:
1. :func:`to_onnx ` accepts the model as a file path
or raw bytes together with a tuple of representative *numpy* inputs (used
to determine input dtypes and shapes when they are not fully specified in
the TFLite model).
2. :func:`~yobx.litert.litert_helper.parse_tflite_model` reads the binary
FlatBuffer and returns a :class:`~yobx.litert.litert_helper.TFLiteModel`
object containing a list of
:class:`~yobx.litert.litert_helper.TFLiteSubgraph` objects, each with its
tensors and operators.
3. A fresh :class:`~yobx.xbuilder.GraphBuilder` is created and
:func:`~yobx.litert.convert._convert_subgraph` walks the operator list:
a. **Weight tensors** (tensors that carry buffer data and are not graph
inputs) are registered as ONNX initializers.
b. **Input tensors** are registered via
:meth:`~yobx.xbuilder.GraphBuilder.make_tensor_input`.
c. Every operator is dispatched to a registered converter (or to an entry
in ``extra_converters``).
4. The ONNX outputs are declared with
:meth:`~yobx.xbuilder.GraphBuilder.make_tensor_output`.
5. :meth:`GraphBuilder.to_onnx ` finalises
and returns the :class:`onnx.ModelProto`.
Quick example
=============
.. code-block:: python
import numpy as np
from yobx.litert import to_onnx
X = np.random.rand(1, 4).astype(np.float32)
onx = to_onnx("model.tflite", (X,))
TFLite FlatBuffer parser
========================
:mod:`yobx.litert.litert_helper` contains a self-contained, zero-dependency
FlatBuffer reader (:class:`~yobx.litert.litert_helper._FlatBuf`) together
with the parsed data-classes
(:class:`~yobx.litert.litert_helper.TFLiteModel`,
:class:`~yobx.litert.litert_helper.TFLiteSubgraph`,
:class:`~yobx.litert.litert_helper.TFLiteTensor`,
:class:`~yobx.litert.litert_helper.TFLiteOperator`) and the
:class:`~yobx.litert.litert_helper.BuiltinOperator` enum.
.. runpython::
:showcode:
from yobx.litert.litert_helper import (
_make_sample_tflite_model,
parse_tflite_model,
)
model = parse_tflite_model(_make_sample_tflite_model())
sg = model.subgraphs[0]
print(f"subgraphs : {len(model.subgraphs)}")
print(f"tensors : {[t.name for t in sg.tensors]}")
print(f"inputs : {sg.inputs}")
print(f"outputs : {sg.outputs}")
for op in sg.operators:
print(f"operator : {op.name} inputs={op.inputs} outputs={op.outputs}")
Converter registry
==================
The registry is a module-level dictionary
``LITERT_OP_CONVERTERS: Dict[Union[int, str], Callable]`` defined in
:mod:`yobx.litert.register`. Keys are
:class:`~yobx.litert.litert_helper.BuiltinOperator` integers
(e.g. ``BuiltinOperator.RELU`` = ``19``); custom ops use their string name.
Values are converter callables.
Registering a converter
-----------------------
Use the :func:`~yobx.litert.register.register_litert_op_converter` decorator.
Pass a single op-code integer, a custom-op name string, or a tuple thereof:
.. code-block:: python
from yobx.litert.register import register_litert_op_converter
from yobx.litert.litert_helper import BuiltinOperator
@register_litert_op_converter(BuiltinOperator.RELU)
def convert_relu(g, sts, outputs, op):
return g.op.Relu(op.inputs[0], outputs=outputs, name="relu")
Converter function signature
============================
Every op converter follows the same contract:
``(g, sts, outputs, op) → output_name``
============= =====================================================
Parameter Description
============= =====================================================
``g`` :class:`GraphBuilder `
— call ``g.op.(…)`` to emit ONNX nodes.
``sts`` ``Dict`` of metadata (currently always ``{}``).
``outputs`` ``List[str]`` of pre-allocated output tensor names
that the converter **must** write to.
``op`` An :class:`~yobx.litert.convert._OpProxy` whose
``inputs`` and ``outputs`` are **string names** of
the tensors, and ``builtin_options`` is a decoded
attribute dict.
============= =====================================================
``op.inputs[i]`` is the ONNX tensor name (a string, not an integer index)
of the *i*-th operator input. Use it directly in ``g.op.*()`` calls.
Dynamic shapes
==============
By default :func:`to_onnx ` marks axis 0 of every
input as dynamic (unnamed batch dimension). To control which axes are
dynamic, pass ``dynamic_shapes`` — a tuple of one ``Dict[int, str]`` per
input where keys are axis indices and values are symbolic dimension names:
.. code-block:: python
onx = to_onnx("model.tflite", (X,), dynamic_shapes=({0: "batch"},))
Custom op converters
====================
The ``extra_converters`` parameter of :func:`to_onnx `
accepts a mapping from :class:`~yobx.litert.litert_helper.BuiltinOperator`
integer (or custom-op name string) to converter function. Entries here take
**priority** over the built-in registry:
.. code-block:: python
import numpy as np
from yobx.litert import to_onnx
from yobx.litert.litert_helper import BuiltinOperator
def custom_relu(g, sts, outputs, op):
"""Replace RELU with Clip(0, 6)."""
return g.op.Clip(
op.inputs[0],
np.array(0.0, dtype=np.float32),
np.array(6.0, dtype=np.float32),
outputs=outputs,
name="relu6",
)
onx = to_onnx("model.tflite", (X,),
extra_converters={BuiltinOperator.RELU: custom_relu})
Adding a new built-in converter
================================
1. Create a new file under ``yobx/litert/ops/`` (e.g. ``yobx/litert/ops/cast_ops.py``).
2. Implement a converter function following the signature above.
3. Decorate it with ``@register_litert_op_converter(BuiltinOperator.CAST)``.
4. Import the new module inside the ``register()`` function in
``yobx/litert/ops/__init__.py``.
.. code-block:: python
# yobx/litert/ops/cast_ops.py
from onnx import TensorProto
from ..register import register_litert_op_converter
from ..litert_helper import BuiltinOperator
from ...xbuilder import GraphBuilder
@register_litert_op_converter(BuiltinOperator.CAST)
def convert_cast(g: GraphBuilder, sts: dict, outputs: list, op) -> str:
"""TFLite CAST → ONNX Cast."""
# op.builtin_options carries "in_data_type" / "out_data_type" integers.
out_type = op.builtin_options.get("out_data_type", 0)
from yobx.litert.litert_helper import litert_dtype_to_np_dtype
import numpy as np
np_dtype = litert_dtype_to_np_dtype(out_type)
onnx_dtype = TensorProto.FLOAT # map np_dtype → TensorProto int
return g.op.Cast(op.inputs[0], to=onnx_dtype, outputs=outputs, name="cast")
Supported ops
=============
See :ref:`l-design-litert-supported-ops` for the full list of
built-in LiteRT op converters, generated automatically from the live registry.