Migrating from onnx.helper to onnx_ir APIs¶

This page shows practical migrations from common onnx.helper model-building patterns to onnx_ir APIs.

For a native IR-first walkthrough without the protobuf comparison, see Constructing models.

Why migrate¶

onnx_ir keeps ONNX concepts (Model/Graph/Node/Value), but gives you:

  • More convenient constructors (ir.val, ir.node, ir.tensor)

  • Better graph mutation ergonomics

  • Utilities for value replacement, extraction, and transformation workflows

  • Memory efficiency when handling large tensors and large models

Mapping cheatsheet¶

onnx.helper pattern

onnx_ir pattern

make_tensor_value_info

ir.val(name, dtype=..., shape=...)

make_tensor

ir.tensor(...)

make_node

ir.node(op_type, inputs, attributes=...)

make_graph

ir.Graph(inputs=..., outputs=..., nodes=..., initializers=...)

make_model

ir.Model(graph, ir_version=...)

onnx.save(model_proto, path)

ir.save(model, path)

Example 1: Build a small model from scratch¶

With onnx.helper¶

import onnx
from onnx import TensorProto

x = onnx.helper.make_tensor_value_info("x", TensorProto.FLOAT, [2, 3])
out = onnx.helper.make_tensor_value_info("out", TensorProto.FLOAT, [2, 3])

bias = onnx.helper.make_tensor(
    "bias",
    TensorProto.FLOAT,
    dims=[2, 3],
    vals=[1.0] * 6,
)

add = onnx.helper.make_node("Add", inputs=["x", "bias"], outputs=["tmp"], name="add_bias")
relu = onnx.helper.make_node("Relu", inputs=["tmp"], outputs=["out"], name="relu")

graph = onnx.helper.make_graph(
    [add, relu],
    "g",
    inputs=[x],
    outputs=[out],
    initializer=[bias],
)
model = onnx.helper.make_model(graph, opset_imports=[onnx.helper.make_opsetid("", 20)])

With onnx_ir¶

import onnx_ir as ir

x = ir.val("x", dtype=ir.DataType.FLOAT, shape=[2, 3])
out = ir.val("out", dtype=ir.DataType.FLOAT, shape=[2, 3])

bias_tensor = ir.tensor(
    [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]],
    dtype=ir.DataType.FLOAT,
)
bias = ir.val("bias", const_value=bias_tensor)

add = ir.node("Add", inputs=[x, bias], name="add_bias")
relu = ir.node("Relu", inputs=add.outputs, outputs=[out], name="relu")

graph = ir.Graph(
    inputs=[x],
    outputs=[out],
    nodes=[add, relu],
    initializers=[bias],
    opset_imports={"": 20},
    name="g",
)
model = ir.Model(graph, ir_version=10)

ir.save(model, "model.onnx")

Example 2: Create an initializer¶

An initializer is a named Value with a constant tensor value that is registered with a graph. Create one in two steps:

import onnx_ir as ir

weight_tensor = ir.tensor(
    [[1.0, 0.0], [0.0, 1.0]],
    dtype=ir.DataType.FLOAT,
)
weight = ir.val("weight", const_value=weight_tensor)

Pass the value to onnx_ir.Graph when constructing a graph:

x = ir.val("x", dtype=ir.DataType.FLOAT, shape=[2, 2])
out = ir.val("out", dtype=ir.DataType.FLOAT, shape=[2, 2])
matmul = ir.node("MatMul", inputs=[x, weight], outputs=[out])

graph = ir.Graph(
    inputs=[x],
    outputs=[out],
    nodes=[matmul],
    initializers=[weight],
    opset_imports={"": 20},
    name="g",
)

Alternatively, omit initializers=[weight] from the constructor and register the value afterward:

graph.register_initializer(weight)

The initializer must have a non-empty name, a const_value, and no producing node. Specify dtype when constructing a tensor from Python values if the ONNX operator requires a particular element type. You can also pass a NumPy array to onnx_ir.tensor(); in that case its dtype is preserved.

Example 3: Create nodes with Python attributes directly¶

With onnx.helper, attributes often require explicit helper calls. With ir.node, plain Python values are converted automatically.

import onnx_ir as ir

x = ir.val("x", dtype=ir.DataType.FLOAT, shape=[1, 3, 8, 8])

conv = ir.node(
    "Conv",
    inputs=[x, ir.val("w"), ir.val("b")],
    attributes={
        "kernel_shape": [3, 3],
        "pads": [1, 1, 1, 1],
        "strides": [1, 1],
        "group": 1,
    },
    name="conv0",
)

Example 4: Graph rewrite (replace a node output)¶

This is a common migration pain-point when using protobuf-level APIs directly.

import onnx_ir as ir

model = ir.load("model.onnx")
graph = model.graph

# Suppose we replace a node producing old_out with a new node producing new_out.
old_node = next(node for node in graph if node.name == "old_node")
inp = old_node.inputs[0]
new_node = ir.node("Identity", [inp], name="new_node")
graph.insert_after(old_node, [new_node])

# Redirect all downstream users and graph outputs.
ir.convenience.replace_all_uses_with(
    old_node.outputs,
    new_node.outputs,
    replace_graph_outputs=True,
)

graph.remove([old_node], safe=True)
ir.save(model, "rewritten.onnx")

Example 5: Extract a bounded subgraph¶

import onnx_ir as ir

model = ir.load("model.onnx")

subgraph = ir.convenience.extract(
    model.graph,
    inputs=["input_0", "weight_0"],
    outputs=["layer3_out"],
)

submodel = ir.Model(subgraph, ir_version=model.ir_version)
ir.save(submodel, "subgraph.onnx")

Migration tips¶

  1. Start by replacing make_tensor_value_info/make_node with ir.val/ir.node.

  2. Keep names explicit while migrating to preserve external interfaces.

  3. Prefer value-based rewrites (replace_all_uses_with) over positional list surgery.

  4. Use ir.save/ir.load at the boundaries and keep transformation logic in IR.

  5. Preserve names, topological order, and type/shape information during rewrites. Run the corresponding repair or validation pass only when needed.