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
.onnxfile 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 |
|---|---|---|
|
|
|
|
|
Same as above, but replaces large
initializers (> 16 elements) with
|
|
|
Compact single nested expression instead of assembling lists |
|
|
Fluent |
|
|
|
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:
START— emitter initialises its state (opset list, ir_version).BEGIN_GRAPH— emitter declares graph-level containers.INITIALIZER× N — one event per initializer tensor.BEGIN_SIGNATURE— separator before inputs.INPUT× N — one event per graph input.END_SIGNATURE— separator after inputs.NODE× N — one event per operator node.BEGIN_RETURN— separator before outputs.OUTPUT× N — one event per graph output.END_RETURN— separator after outputs.END_GRAPH— emitter assembles the graph object.Optionally, for each local function:
BEGIN_FUNCTION…END_FUNCTION.TO_ONNX_MODEL— emitter assembles the finalModelProto.
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.