Tutorial showing inference of a Graph Neural Network with SOFIE.
A graph network model following DeepMind's Encode-Process-Decode architecture (see arXiv:1806.01261) is defined in PyTorch and exported to ONNX. The ONNX models are then parsed with the SOFIE ONNX parser, C++ inference code is generated and compiled, and its output is validated against PyTorch.
import time
import numpy as np
import ROOT
import torch
num_nodes = 5
num_edges = 20
snd =
np.array([1, 2, 3, 4, 2, 3, 4, 3, 4, 4, 0, 0, 0, 0, 1, 1, 1, 2, 2, 3], dtype=
"int64")
rec =
np.array([0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 1, 2, 3, 4, 2, 3, 4, 3, 4, 4], dtype=
"int64")
node_size = 4
edge_size = 4
global_size = 1
LATENT_SIZE = 100
NUM_LAYERS = 4
processing_steps = 5
numevts = 40
def get_graph_data_dict(num_nodes, num_edges, NODE_FEATURE_SIZE=2, EDGE_FEATURE_SIZE=2, GLOBAL_FEATURE_SIZE=1):
return {
"senders": snd,
"receivers": rec,
}
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"]
sample_input += sample_indices
input_names += ["receivers", "senders"]
component,
sample_input,
name + ".onnx",
input_names=input_names,
output_names=["node_output", "edge_output", "global_output"],
dynamo=True,
)
for name in ["gnn_encoder", "gnn_core", "gnn_decoder", "gnn_output_transform"]:
gen_code = """#pragma cling optimize(2)
#include "gnn_encoder.hxx"
#include "gnn_core.hxx"
#include "gnn_decoder.hxx"
#include "gnn_output_transform.hxx"
"""
def __init__(self):
@staticmethod
return (
)
def infer(self, graphData):
n_nodes =
len(graphData[
"nodes"])
n_edges =
len(graphData[
"edges"])
latent = self._as_arrays(
self.encoder_session.
infer(
c(graphData[
"nodes"]),
c(graphData[
"edges"]),
c(graphData[
"globals"])),
n_nodes, n_edges,
)
latent0 = latent
output_ops = []
for _
in range(processing_steps):
latent = self._as_arrays(
self.core_session.
infer(
c(core_input[0]),
c(core_input[1]),
c(core_input[2]), receivers, senders),
n_nodes, n_edges,
)
decoded = self._as_arrays(
self.decoder_session.
infer(
c(latent[0]),
c(latent[1]),
c(latent[2])), n_nodes, n_edges
)
self._as_arrays(
self.output_transform_session.
infer(
c(decoded[0]),
c(decoded[1]),
c(decoded[2])),
n_nodes, n_edges,
)
)
return output_ops
processing_steps,
)
hG =
ROOT.TH1D(
"hG",
"Result from PyTorch", 20, 1, 0)
torchOutput = []
print("elapsed time for ", numevts, "events = ", end - start)
hS =
ROOT.TH1D(
"hS",
"Result from SOFIE", 20, 1, 0)
print("time to create SOFIE GNN class", start - start0)
sofieOutput = []
print("elapsed time for ", numevts, "events = ", end - start)
hDn =
ROOT.TH1D(
"hDn",
"Difference for node data", 40, 1, 0)
hDe =
ROOT.TH1D(
"hDe",
"Difference for edge data", 40, 1, 0)
hDg =
ROOT.TH1D(
"hDg",
"Difference for global data", 40, 1, 0)
maxDifference = 0.0
for hist, j in [(hDn, 0), (hDe, 1), (hDg, 2)]:
difference = sofieOutput[i][1][j] - torchOutput[i][1][j]
maxDifference = max(maxDifference,
np.abs(difference).max())
print("maximum difference between SOFIE and PyTorch = ", maxDifference)
if maxDifference > 1e-4:
ROOT::Detail::TRangeCast< T, true > TRangeDynCast
TRangeDynCast is an adapter class that allows the typed iteration through a TCollection.
Option_t Option_t TPoint TPoint const char GetTextMagnitude GetFillStyle GetLineColor GetLineWidth GetMarkerStyle GetTextAlign GetTextColor GetTextSize void char Point_t Rectangle_t WindowAttributes_t Float_t Float_t Float_t Int_t Int_t UInt_t UInt_t Rectangle_t Int_t Int_t Window_t TString Int_t GCValues_t GetPrimarySelectionOwner GetDisplay GetScreen GetColormap GetNativeEvent const char const char dpyName wid window const char font_name cursor keysym reg const char only_if_exist regb h Point_t winding char text const char depth char const char Int_t count const char ColorStruct_t color const char Pixmap_t Pixmap_t PictureAttributes_t attr const char char ret_data h unsigned char height h Atom_t Int_t ULong_t ULong_t unsigned char prop_list Atom_t Atom_t Atom_t Time_t UChar_t len