Tutorial parsing a Graph Neural Network from ONNX and generating SOFIE inference code.
A graph network model following DeepMind's Encode-Process-Decode architecture (see arXiv:1806.01261) is defined in PyTorch and exported to ONNX with dynamic node and edge counts. The SOFIE ONNX parser then generates C++ inference code for the four component networks. The tutorial also generates input data, evaluated here with PyTorch as a reference, which serves as input for the tutorial TMVA_SOFIE_GNN_Application.C.
import time
import numpy as np
import ROOT
import torch
num_max_nodes = 100
num_max_edges = 300
node_size = 4
edge_size = 4
global_size = 1
LATENT_SIZE = 100
NUM_LAYERS = 4
processing_steps = 5
numevts = 100
return {
}
layers = []
for _
in range(NUM_LAYERS):
num_inputs = LATENT_SIZE
if with_layer_norm:
def __init__(self, num_node_inputs, num_edge_inputs, num_global_inputs):
def forward(self, node_data, edge_data, global_data):
return self.node_fn(node_data), self.edge_fn(edge_data), self.global_fn(global_data)
def __init__(self, num_node_inputs, num_edge_inputs, num_global_inputs):
self.edge_fn =
make_mlp_model(num_edge_inputs + 2 * num_node_inputs + num_global_inputs,
True)
self.node_fn =
make_mlp_model(LATENT_SIZE + num_node_inputs + num_global_inputs,
True)
self.global_fn =
make_mlp_model(2 * LATENT_SIZE + num_global_inputs,
True)
def forward(self, node_data, edge_data, global_data, receivers, senders):
[edge_data, node_data[receivers], node_data[senders],
global_data.expand(n_edges, -1)], dim=1
)
edge_output = self.edge_fn(edge_input)
)
node_output = self.node_fn(node_input)
)
global_output = self.global_fn(global_input)
return node_output, edge_output, global_output
def __init__(self):
self._core =
MLPGraphNetwork(2 * LATENT_SIZE, 2 * LATENT_SIZE, 2 * LATENT_SIZE)
def forward(self, node_data, edge_data, global_data, receivers, senders, num_processing_steps):
latent = self._encoder(node_data, edge_data, global_data)
latent0 = latent
output_ops = []
for _
in range(num_processing_steps):
latent = self._core(*core_input, receivers, senders)
decoded_op = self._decoder(*latent)
return output_ops
sample_input = (
)
input_names = ["node_data", "edge_data", "global_data"]
dynamic_shapes = {
"node_data": {0: num_nodes_dim},
"edge_data": {0: num_edges_dim},
"global_data": None,
}
sample_input += (
)
input_names += ["receivers", "senders"]
component,
sample_input,
name + ".onnx",
input_names=input_names,
output_names=["node_output", "edge_output", "global_output"],
dynamic_shapes=dynamic_shapes,
dynamo=True,
)
for name in ["encoder", "core", "decoder", "output_transform"]:
print("generated SOFIE model", name + ".hxx")
tree.Branch(
"node_data",
"std::vector<float>", node_data)
tree.Branch(
"edge_data",
"std::vector<float>", edge_data)
tree.Branch(
"global_data",
"std::vector<float>", global_data)
tree.Branch(
"receivers",
"std::vector<int>", receivers)
print("\n\nSaving data in a ROOT File:")
h1 =
ROOT.TH1D(
"h1",
"GNN nodes output", 40, 1, 0)
h2 =
ROOT.TH1D(
"h2",
"GNN edges output", 40, 1, 0)
h3 =
ROOT.TH1D(
"h3",
"GNN global output", 40, 1, 0)
dataset = []
for graphData in dataset:
processing_steps,
)
print("time to evaluate ", numevts, " events", end - start)
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.