Constructing models¶

ONNX IR provides Python-native constructors for values, tensors, nodes, graphs, and models. Construct objects directly when the graph structure is explicit, or use onnx_ir.tape.Tape when building a longer sequence of operations.

Build a model directly¶

Create graph inputs and outputs with onnx_ir.val(). Static dimensions are integers; strings create symbolic dimensions.

import onnx_ir as ir

x = ir.val("x", dtype=ir.DataType.FLOAT, shape=["batch", 4])
y = ir.val("y", dtype=ir.DataType.FLOAT, shape=["batch", 4])

Initializers are values with constant tensor data:

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

Create nodes with onnx_ir.node(). Plain Python attribute values are converted to ONNX attributes automatically.

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

Assemble the graph and model:

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

Values and nodes are connected by object identity. The strings used as names are for serialization, diagnostics, and lookup; they do not create graph edges.

Add nodes incrementally¶

Graph mutation methods establish ownership, assign missing names, and maintain use-def relationships:

old_output = graph.outputs[0]
old_output.name = "relu_output"
new_output = ir.val("y", type=old_output.type, shape=old_output.shape)

sigmoid = ir.node("Sigmoid", inputs=[old_output], outputs=[new_output])
graph.append(sigmoid)
graph.outputs[0] = new_output

A node can belong to only one graph. Remove it from its current graph before moving it to another graph.

Use graph.register_initializer(value) to add an initializer after graph construction. The value must be named, have const_value, and have no producer.

Use Tape for sequential construction¶

onnx_ir.tape.Tape records nodes and initializers while returning values that can be passed directly to later operations:

tape = ir.tape.Tape()

weight = tape.initializer(
    ir.tensor(
        [[1.0, 0.0], [0.0, 1.0]],
        dtype=ir.DataType.FLOAT,
        name="weight",
    )
)
x = ir.val("x", dtype=ir.DataType.FLOAT, shape=["batch", 2])

matmul = tape.op("MatMul", [x, weight])
output = tape.op("Relu", [matmul])

graph = ir.Graph(
    inputs=[x],
    outputs=[output],
    nodes=tape.nodes,
    initializers=tape.initializers,
    opset_imports={"": 21},
    name="main_graph",
)
model = ir.Model(graph, ir_version=10)

Use op_multi_out for operators with multiple outputs. tape.used_opsets records the domain and explicit version requested by each operation, but callers still choose the graph’s final opset imports.

Preserve public interfaces¶

Graph input and output names are part of a model’s external interface. Keep them explicit when constructing or rewriting models. Intermediate names may be omitted and assigned automatically when nodes enter a graph.

Run onnx_ir.passes.common.NameFixPass only when construction may have introduced missing or duplicate names:

import onnx_ir.passes.common as common_passes

model = common_passes.NameFixPass()(model).model

Preserve construction invariants¶

Construction APIs intentionally permit intermediate states that may not yet form a valid ONNX model. Build nodes in topological order, keep public names unique, and provide required type information directly when possible.

Use targeted passes only for invariants the construction process did not preserve. See Preserve invariants and use targeted repair for the authoritative checklist.

At an explicit validation boundary, the checker can confirm the completed model:

common_passes.CheckerPass(full_check=True)(model)

See Model I/O and external data workflows for shape inference and serialization options.