Translate#

translate converts an existing onnx.ModelProto into Python source code that, when executed, recreates the same graph. This is useful for:

  • debugging — turn an opaque .onnx file into readable Python so you can inspect or modify individual nodes;

  • sharing — paste a self-contained snippet into a bug report or a unit test without attaching a binary file;

  • migration — port a model written with one builder API to another.

Architecture overview#

The translation is split into two independent layers:

ModelProto / GraphProto / FunctionProto
       │
       ▼
   Translator          ← walks the proto, fires events
       │
       ▼
    Emitter            ← converts events into code strings

Translator knows the structure of ONNX protos; it never emits text directly. Instead it fires a sequence of typed events (BEGIN_GRAPH, INITIALIZER, NODE, END_GRAPH, …) defined by EventType.

BaseEmitter receives those events and returns lists of code strings. Each concrete emitter subclass overrides the _emit_* methods to produce code in the desired target API. The default implementation raises NotImplementedError for every event, so any missing override is caught early.

<<<

from yobx.translate.base_emitter import EventType

for name, value in sorted(EventType.__members__.items(), key=lambda kv: kv[1]):
    print(f"{value:2d}  {name}")

>>>

     0  START
     1  INPUT
     2  OUTPUT
     3  NODE
     4  TO_ONNX_MODEL
     5  BEGIN_GRAPH
     6  END_GRAPH
     7  BEGIN_FUNCTION
     8  END_FUNCTION
     9  INITIALIZER
    10  SPARSE_INITIALIZER
    11  FUNCTION_INPUT
    12  FUNCTION_OUTPUT
    13  FUNCTION_ATTRIBUTES
    14  TO_ONNX_FUNCTION
    15  BEGIN_SIGNATURE
    16  END_SIGNATURE
    17  BEGIN_RETURN
    18  END_RETURN
    19  BEGIN_FUNCTION_SIGNATURE
    20  END_FUNCTION_SIGNATURE
    21  BEGIN_FUNCTION_RETURN
    22  END_FUNCTION_RETURN

Available emitters#

Five concrete emitters are shipped:

Class

Short name

Output API

InnerEmitter

"onnx"

onnx.helper (oh.make_*)

InnerEmitterShortInitializer

"onnx-short"

Same as above, but replaces large initializers (> 16 elements) with np.random.randn(…)

InnerEmitterCompact

"onnx-compact"

Compact single nested expression instead of assembling lists

LightEmitter

"light"

Fluent start(…).vin(…).… method chain

BuilderEmitter

"builder"

GraphBuilder-based function wrapper

The five APIs are exposed through the single convenience function translate:

<<<

import onnx.helper as oh
import onnx
from yobx.translate import translate

model = oh.make_model(
    oh.make_graph(
        [
            oh.make_node("Add", ["X", "Y"], ["T"]),
            oh.make_node("Relu", ["T"], ["Z"]),
        ],
        "add_relu",
        [
            oh.make_tensor_value_info("X", onnx.TensorProto.FLOAT, [None, 4]),
            oh.make_tensor_value_info("Y", onnx.TensorProto.FLOAT, [None, 4]),
        ],
        [oh.make_tensor_value_info("Z", onnx.TensorProto.FLOAT, [None, 4])],
    ),
    opset_imports=[oh.make_opsetid("", 17)],
    ir_version=9,
)

code = translate(model, api="onnx")
print(code)

>>>

    opset_imports = [
        oh.make_opsetid('', 17),
    ]
    inputs = []
    outputs = []
    nodes = []
    initializers = []
    sparse_initializers = []
    functions = []
    inputs.append(oh.make_tensor_value_info('X', onnx.TensorProto.FLOAT, shape=(None, 4)))
    inputs.append(oh.make_tensor_value_info('Y', onnx.TensorProto.FLOAT, shape=(None, 4)))
    nodes.append(
        oh.make_node(
            'Add',
            ['X', 'Y'],
            ['T']
        )
    )
    nodes.append(
        oh.make_node(
            'Relu',
            ['T'],
            ['Z']
        )
    )
    outputs.append(oh.make_tensor_value_info('Z', onnx.TensorProto.FLOAT, shape=(None, 4)))
    graph = oh.make_graph(
        nodes,
        'add_relu',
        inputs,
        outputs,
        initializers,
        sparse_initializer=sparse_initializers,
    )
    model = oh.make_model(
        graph,
        functions=functions,
        opset_imports=opset_imports,
        ir_version=9,
    )

Event sequence#

For a ModelProto the Translator fires events in this order:

  1. START — emitter initialises its state (opset list, ir_version).

  2. BEGIN_GRAPH — emitter declares graph-level containers.

  3. INITIALIZER × N — one event per initializer tensor.

  4. BEGIN_SIGNATURE — separator before inputs.

  5. INPUT × N — one event per graph input.

  6. END_SIGNATURE — separator after inputs.

  7. NODE × N — one event per operator node.

  8. BEGIN_RETURN — separator before outputs.

  9. OUTPUT × N — one event per graph output.

  10. END_RETURN — separator after outputs.

  11. END_GRAPH — emitter assembles the graph object.

  12. Optionally, for each local function: BEGIN_FUNCTIONEND_FUNCTION.

  13. TO_ONNX_MODEL — emitter assembles the final ModelProto.

FunctionProto inputs and outputs use the FUNCTION_INPUT / FUNCTION_OUTPUT variants and the sequence is bookended by BEGIN_FUNCTION / END_FUNCTION instead of BEGIN_GRAPH / END_GRAPH.

InnerEmitter#

InnerEmitter produces standard onnx.helper code. Every initializer is written as an exact np.array(…) literal.

InnerEmitterShortInitializer inherits from InnerEmitter and overrides only _emit_initializer: tensors with more than 16 elements are replaced by a np.random.randn(…) or np.random.randint(…) call, so the snippet stays readable for large weight matrices.

<<<

import numpy as np
import onnx.helper as oh
import onnx.numpy_helper as onh
import onnx
from yobx.translate import translate

big_w = onh.from_array(np.random.randn(8, 5).astype(np.float32), name="W")
model = oh.make_model(
    oh.make_graph(
        [oh.make_node("MatMul", ["X", "W"], ["Z"])],
        "mm",
        [oh.make_tensor_value_info("X", onnx.TensorProto.FLOAT, [None, 8])],
        [oh.make_tensor_value_info("Z", onnx.TensorProto.FLOAT, [None, 5])],
        [big_w],
    ),
    opset_imports=[oh.make_opsetid("", 17)],
)
print(translate(model, api="onnx-short"))

>>>

    opset_imports = [
        oh.make_opsetid('', 17),
    ]
    inputs = []
    outputs = []
    nodes = []
    initializers = []
    sparse_initializers = []
    functions = []
    value = np.random.randn(8, 5).astype(np.float32)
    initializers.append(
        onh.from_array(
            np.array(value, dtype=np.float32),
            name='W'
        )
    )
    inputs.append(oh.make_tensor_value_info('X', onnx.TensorProto.FLOAT, shape=(None, 8)))
    nodes.append(
        oh.make_node(
            'MatMul',
            ['X', 'W'],
            ['Z']
        )
    )
    outputs.append(oh.make_tensor_value_info('Z', onnx.TensorProto.FLOAT, shape=(None, 5)))
    graph = oh.make_graph(
        nodes,
        'mm',
        inputs,
        outputs,
        initializers,
        sparse_initializer=sparse_initializers,
    )
    model = oh.make_model(
        graph,
        functions=functions,
        opset_imports=opset_imports,
        ir_version=13,
    )

InnerEmitterCompact#

InnerEmitterCompact inherits from InnerEmitter and produces a compact single nested expression instead of assembling separate lists of nodes, inputs, and outputs.

<<<

import onnx.helper as oh
import onnx
from yobx.translate import translate

model = oh.make_model(
    oh.make_graph(
        [oh.make_node("Relu", ["X"], ["Z"])],
        "relu",
        [oh.make_tensor_value_info("X", onnx.TensorProto.FLOAT, [None, 4])],
        [oh.make_tensor_value_info("Z", onnx.TensorProto.FLOAT, [None, 4])],
    ),
    opset_imports=[oh.make_opsetid("", 17)],
)
print(translate(model, api="onnx-compact"))

>>>

    model = oh.make_model(
        oh.make_graph(
            [
                oh.make_node('Relu', ['X'], ['Z']),
            ],
            'relu',
            [
                oh.make_tensor_value_info('X', onnx.TensorProto.FLOAT, (None, 4)),
            ],
            [
                oh.make_tensor_value_info('Z', onnx.TensorProto.FLOAT, (None, 4)),
            ],
        ),
        functions=[],
        opset_imports=[oh.make_opsetid('', 17)],
        ir_version=13,
    )

LightEmitter#

LightEmitter generates a fluent method chain (start(…).vin(…).…). The chain is either written as a multi-line indented block (default) or collapsed to a single .-joined line when single_line=True.

<<<

import onnx.helper as oh
import onnx
from yobx.translate import translate

model = oh.make_model(
    oh.make_graph(
        [oh.make_node("Relu", ["X"], ["Z"])],
        "relu",
        [oh.make_tensor_value_info("X", onnx.TensorProto.FLOAT, [None, 4])],
        [oh.make_tensor_value_info("Z", onnx.TensorProto.FLOAT, [None, 4])],
    ),
    opset_imports=[oh.make_opsetid("", 17)],
)
print(translate(model, api="light"))

>>>

    (
        start(opset=17)
        .vin('X', elem_type=onnx.TensorProto.FLOAT, shape=(None, 4))
        .bring('X')
        .Relu()
        .rename('Z')
        .bring('Z')
        .vout(elem_type=onnx.TensorProto.FLOAT, shape=(None, 4))
        .to_onnx()
    )

BuilderEmitter#

BuilderEmitter generates code that uses GraphBuilder. The graph body is wrapped in a Python function named after the ONNX graph, and a small driver block constructs the GraphBuilder, calls the function, and finalises the model with g.to_onnx().

<<<

import onnx.helper as oh
import onnx
from yobx.translate import translate

model = oh.make_model(
    oh.make_graph(
        [oh.make_node("Relu", ["X"], ["Z"])],
        "relu",
        [oh.make_tensor_value_info("X", onnx.TensorProto.FLOAT, [None, 4])],
        [oh.make_tensor_value_info("Z", onnx.TensorProto.FLOAT, [None, 4])],
    ),
    opset_imports=[oh.make_opsetid("", 17)],
)
print(translate(model, api="builder"))

>>>

    
    def relu(
        op: "GraphBuilder",
        X: "FLOAT[None, 4]",
    ):
        Z = op.Relu(X, outputs=['Z'])
        op.Identity(Z, outputs=["Z"])
        return Z
    
    g = GraphBuilder({'': 17}, ir_version=13)
    g.make_tensor_input("X", onnx.TensorProto.FLOAT, (None, 4))
    relu(g.op, "X")
    g.make_tensor_output("Z", onnx.TensorProto.FLOAT, (None, 4), is_dimension=False, indexed=False)
    model = g.to_onnx()

Round-trip verification#

The generated "onnx" code is fully self-contained; running it recreates the original model:

<<<

import numpy as np
import onnx
import onnx.helper as oh
from yobx.translate import translate, translate_header

model = oh.make_model(
    oh.make_graph(
        [
            oh.make_node("Transpose", ["X"], ["Y"], perm=[1, 0]),
        ],
        "transpose",
        [oh.make_tensor_value_info("X", onnx.TensorProto.FLOAT, [3, 4])],
        [oh.make_tensor_value_info("Y", onnx.TensorProto.FLOAT, [4, 3])],
    ),
    opset_imports=[oh.make_opsetid("", 17)],
    ir_version=9,
)

code = translate(model, api="onnx")
header = translate_header("onnx")
ns: dict = {}
exec(compile(header + "\n" + code, "<translate>", "exec"), ns)  # noqa: S102
recreated = ns["model"]

assert len(recreated.graph.node) == len(model.graph.node)
print("nodes       :", len(recreated.graph.node))
print("opset       :", recreated.opset_import[0].version)
print("ir_version  :", recreated.ir_version)
print("Round-trip  : OK")

>>>

    nodes       : 1
    opset       : 17
    ir_version  : 9
    Round-trip  : OK

See also

Comparing the four ONNX translation APIs — sphinx-gallery example that builds a Gemm Relu model, translates it with all five APIs, prints the generated snippets, verifies the round-trip, and plots a bar chart of generated code sizes.