11. popart.ir
User Guide (experimental)
Warning
The popart.ir
Python module is currently experimental and may be subject to change
in future releases in ways that are backwards incompatible without
deprecation warnings.
Warning
Due to the experimental nature of popart.ir
the documentation provided in
this section is incomplete.
As an alternative to using the ONNX builder to create models, popart.ir
is
an experimental PopART Python module which you can use to create
(and, to a limited degree, manipulate) PopART models directly.
PopART models are represented using an intermediate representation (IR).
The popart.ir
package allows you to manipulate these IRs.
11.1. Concepts
11.1.1. Building blocks

Fig. 11.1 An IR contains a main graph (MG) and multiple other graphs (G). Graphs can contain ops, intermediate tensors (T) and constant tensors (C). The main graph can also contain intermediate, constant and variable tensors (V).
The building blocks of popart.ir
are IRs, graphs, tensors and ops. This section summaries these concepts, further information
on each topic can be found later in the user guide.
IRs
An IR is an executable program that can be run using a PopART session and a Python process can initialise multiple IRs. An IR contains one main graph, created on IR initialisation, and multiple other subgraphs that you create.
Graphs

Fig. 11.2 In this example the IR’s main graph (MG) calls subgraph 1 (G1) which in turn calls subgraph 2 (G2). This creates a call tree which is depicted on the right. Op nodes are green, intermediate tensors are red and constants are yellow.
A graphs describes a computational directed acyclic graph (DAG) which contains two types of nodes: tensors and ops. There are two types of graphs: the main graph and subgraphs.
The main graph is a special graph and only one exists per IR. It is the entry point of a IR (like the main function in many programming languages). The main graph can contain intermediate, constant and variable tensors.
Subgraphs have input and output tensors. Subgraphs can be called by other graphs using the
call
orrepeat
op. If a subgraph has multiple call sites, the subgraph is outlined during lowering, leading to code reuse and reduced memory usage. A subgraph can only contain intermediate or constant tensors and not variables. Subgraphs have intermediate tensors which are marked as inputs or outputs. When a subgraph is called the inputs must be provided by the calling graph. The input data can be either be passed by reference or value, and this is determined by the user at the call site.
Tensors
Tensors have a shape and datatype, and sometimes initialisation data. A tensor will be produced by an op known as the producer and can have multiple consumer ops. There are three types of tensors: intermediate, variable and constant. Variables and constants are initialised with data.
Constants contain data that cannot change.
Variables contain data that are always live and hence is never freed. Typically model weights are kept on device between runs and are therefore defined as variable tensors.
Intermediates are not initialised with data and are live from the time they are produced until their final consumer.
Ops
An op represents an operation in the computational graph and can have input and output tensors.
11.2. Simple example
To use popart.ir
you first need to import it as a Python package:
import popart.ir as pir
Note that we typically import popart.ir
as pir
for brevity.
As explained previously, the main purpose of this package is creating and manipulating IRs,
which are represented by the class pir.Ir
.
See below for a basic example of how to construct such an object.
import popart.ir as pir
import popart.ir.ops as ops
# Creating a model with popart.ir
ir = pir.Ir()
main = ir.main_graph()
with main:
# host load
input0 = pir.h2d_stream([1], pir.float32, name="input0_stream")
a = ops.host_load(input0, "a")
input1 = pir.h2d_stream([1], pir.float32, name="input1_stream")
b = ops.host_load(input1, "b")
# addition
o = ops.add(a, b)
# host store
o_d2h = pir.d2h_stream(o.shape, o.dtype, name="output_stream")
ops.host_store(o_d2h, o)
files/simple_addition_popart_ir.py
In popart.ir
an IR is essentially a collection of pir.Graph
objects.
Each such graph contains a number of operations.
Each IR has a main graph that is constructed by default.
This main graph serves as the entry point for your model.
A main graph is obtained via ir.main_graph()
in the example above.
By adding operations within a with main
context, the operations
are automatically added to the main graph.
In this example, three operations added: host_load
, add
and host_store
.
In this model we created two device-to-host streams input0
and input1
and one host-to-device stream output
.
The host_load
operations are used to stream data from the host to
the device populating tensors a
and b
, respectively.
Another operation, add
, then sums these two tensors.
Finally, the host_store
streams the result data back from the device to the host.
11.3. Data types
Currently, popart.ir
supports the data types listed in ir_datatypes_table
.
These data types are defined in popart.ir
directly and
will be converted to their IPU-compatible data type. Note that the int64
and uint64
will be downcast to int32
and uint32
respectively
if the session option enableSupportedDataTypeCasting
is set to True
.
popart.ir dtype |
int |
floating point |
signed |
np.dtype |
Python dtype |
alias |
---|---|---|---|---|---|---|
|
False |
False |
False |
np.bool |
builtins.bool |
N/A |
|
True |
False |
True |
np.int8 |
None |
N/A |
|
True |
False |
True |
np.int32 |
None |
N/A |
|
True |
False |
False |
np.uint8 |
None |
N/A |
|
True |
False |
False |
np.uint32 |
None |
N/A |
|
False |
True |
True |
np.float16 |
None |
|
|
False |
True |
True |
np.float32 |
builtins.float |
|
|
False |
True |
True |
np.float64 |
None |
|
11.4. Tensors
You define a tensor with shape, data type and optional initialisation data. A tensor has zero or more consumer operations and up to one producer operation.
- There are three types of tensors in
popart.ir
: Constant
Variable
Intermediate
An intermediate tensor is the output of an operation.
Variables and constants are initialised with data.
For instance, in the example tensor_addition_code
, a
is a variable tensor,
b
is a constant tensor, and o
is an intermediate tensor.
with main:
a = pir.variable(3, dtype=pir.int8, name="variable_a")
b = pir.constant(1, dtype=pir.int8, name="constant_b")
# addition
o = a + b
files/tensor_addition_popart_ir.py
11.4.1. Constant
A constant tensor is initialised with data during graph creation.
This tensor cannot change during the runtime of a model.
You can also use python numeric literals in popart.ir
.
These literals are implicitly converted to constant tensors.
That is, these lines
can also be writen as
o = a + 1
11.4.2. Variable
A variable tensor represents trainable parameters in a model
or non-trainable optimizer states.
You create and initialize variable tensors in the main graph scope.
You can add a variable to the main graph using pir.variable
.
To enable flexible interaction, you can read or write variables on
the IPU at runtime using the session.readWeights()
and
session.writeWeights()
methods respectively.
Therefore, variables are always live in IPU memory and don’t get freed
during execution.
Note that, you have to copy the initial value of a variable to IPU device
from the host before running the graph by using session.weightsFromHost()
.
11.4.3. Intermediate
An intermediate tensor is produced by an operation, which means it is not initialised with data. It stays live in IPU memory from when it is produced until the last time it is consumed.