diff --git a/bindings/pyroot/pythonizations/python/CMakeLists.txt b/bindings/pyroot/pythonizations/python/CMakeLists.txt index 0bddff3c3216b..c3c3819843b8a 100644 --- a/bindings/pyroot/pythonizations/python/CMakeLists.txt +++ b/bindings/pyroot/pythonizations/python/CMakeLists.txt @@ -83,7 +83,9 @@ if(tmva) ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/sigmoid.py ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/softmax.py ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/swish.py - ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py) + ROOT/_pythonization/_tmva/_sofie/_parser/_keras/layers/tanh.py + ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/__init__.py + ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/parser.py) endif() set(py_sources diff --git a/bindings/pyroot/pythonizations/python/ROOT/_facade.py b/bindings/pyroot/pythonizations/python/ROOT/_facade.py index 179944bd4bf2b..b845b77dc852f 100644 --- a/bindings/pyroot/pythonizations/python/ROOT/_facade.py +++ b/bindings/pyroot/pythonizations/python/ROOT/_facade.py @@ -566,9 +566,11 @@ def TMVA(self): from ._pythonization import _tmva # noqa: F401 from ._pythonization._tmva._rtensor import _AsRTensor from ._pythonization._tmva._sofie._parser._keras.parser import PyKeras + from ._pythonization._tmva._sofie._parser._pytorch.parser import PyTorch from ._pythonization._tmva._tree_inference import SaveXGBoost setattr(ns.Experimental.SOFIE, "PyKeras", PyKeras) + setattr(ns.Experimental.SOFIE, "PyTorch", PyTorch) ns.Experimental.AsRTensor = _AsRTensor ns.Experimental.SaveXGBoost = SaveXGBoost diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/__init__.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/__init__.py new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/parser.py b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/parser.py new file mode 100644 index 0000000000000..7a403a2903886 --- /dev/null +++ b/bindings/pyroot/pythonizations/python/ROOT/_pythonization/_tmva/_sofie/_parser/_pytorch/parser.py @@ -0,0 +1,421 @@ +import os +import time + + +def move_operator(op): + """ + Wrap an operator into a std::unique_ptr to pass it to RModel::AddOperator(). + """ + import ROOT + + # If the object is already held by a smart pointer, just move it. + smartptr = op.__smartptr__() + if smartptr: + return type(smartptr)(ROOT.std.move(smartptr)) + + ROOT.SetOwnership(op, False) + return ROOT.std.unique_ptr[type(op)](op) + + +def _node_get(node, key): + """ + Get the value of an attribute of a PyTorch ONNX Graph node. + + The helper function is used to avoid dependency on the onnx submodule (for the + subscript operator of torch._C.Node), as done in https://github.com/pytorch/pytorch/pull/82628 + """ + sel = node.kindOf(key) + return getattr(node, sel)(key) + + +def MakePyTorchGemm(node): + """ + Create a SOFIE Gemm operator from a PyTorch ONNX Graph node. + + For PyTorch's Linear layer having Gemm operation in its ONNX graph, + the names of the input tensor, output tensor are extracted, and then + are passed to instantiate a ROperator_Gemm object using the required attributes. + The node inputs are a list of tensor names, which includes the names of the + input tensor and the weight tensors. + + Parameters: + node (dict): A dictionary containing node information including type, attributes, + input & output tensor names and the data type (must be float). + + Returns: + ROperator_Gemm: A SOFIE framework operator representing the Gemm operation. + """ + from ROOT.TMVA.Experimental import SOFIE + + attributes = node["nodeAttributes"] + inputs = node["nodeInputs"] + outputs = node["nodeOutputs"] + node_dtype = node["nodeDType"][0] + + # Extracting the parameters for the Gemm operator + name_a = inputs[0] + name_b = inputs[1] + name_c = inputs[2] + name_y = outputs[0] + attr_alpha = float(attributes["alpha"]) + attr_beta = float(attributes["beta"]) + + if "transB" in attributes: + attr_transB = int(attributes["transB"]) + attr_transA = int(not attr_transB) + else: + attr_transA = int(attributes["transA"]) + attr_transB = int(not attr_transA) + + if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: + return SOFIE.ROperator_Gemm["float"]( + attr_alpha, attr_beta, attr_transA, attr_transB, name_a, name_b, name_c, name_y + ) + else: + raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Gemm does not yet support input type " + node_dtype) + + +def MakePyTorchConv(node): + """ + Create a SOFIE Conv operator from a PyTorch ONNX Graph node. + + For the Conv operator of PyTorch's ONNX Graph, attributes like dilations, group, + kernel shape, pads and strides are found, and are passed in instantiating the + ROperator object with autopad default to `NOTSET`. + + Parameters: + node (dict): A dictionary containing node information including type, attributes, + input & output tensor names and the data type (must be float). + + Returns: + ROperator_Conv: A SOFIE framework operator representing the Conv operation. + """ + from ROOT.TMVA.Experimental import SOFIE + + attributes = node["nodeAttributes"] + inputs = node["nodeInputs"] + outputs = node["nodeOutputs"] + node_dtype = node["nodeDType"][0] + + # Extracting the Conv node attributes + attr_autopad = "NOTSET" + attr_dilations = list(attributes["dilations"]) + attr_group = int(attributes["group"]) + attr_kernel_shape = list(attributes["kernel_shape"]) + attr_pads = list(attributes["pads"]) + attr_strides = list(attributes["strides"]) + name_x = inputs[0] + name_w = inputs[1] + name_b = inputs[2] + name_y = outputs[0] + + if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: + return SOFIE.ROperator_Conv["float"]( + attr_autopad, + attr_dilations, + attr_group, + attr_kernel_shape, + attr_pads, + attr_strides, + name_x, + name_w, + name_b, + name_y, + ) + else: + raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Conv does not yet support input type " + node_dtype) + + +def MakePyTorchRelu(node): + """ + Create a SOFIE Relu operator from a PyTorch ONNX Graph node. + + For instantiating a ROperator_Relu object, the names of + input & output tensors and the data type of the node are extracted. + + Parameters: + node (dict): A dictionary containing node information including type, attributes, + input & output tensor names and the data type (must be float). + + Returns: + ROperator_Relu: A SOFIE framework operator representing the Relu operation. + """ + from ROOT.TMVA.Experimental import SOFIE + + inputs = node["nodeInputs"] + outputs = node["nodeOutputs"] + node_dtype = node["nodeDType"][0] + + if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: + return SOFIE.ROperator_Relu["float"](inputs[0], outputs[0]) + else: + raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Relu does not yet support input type " + node_dtype) + + +def MakePyTorchSelu(node): + """ + Create a SOFIE Selu operator from a PyTorch ONNX Graph node. + + For instantiating a ROperator_Selu object, the names of + input & output tensors and the data type of the node are extracted. + + Parameters: + node (dict): A dictionary containing node information including type, attributes, + input & output tensor names and the data type (must be float). + + Returns: + ROperator_Selu: A SOFIE framework operator representing the Selu operation. + """ + from ROOT.TMVA.Experimental import SOFIE + + inputs = node["nodeInputs"] + outputs = node["nodeOutputs"] + node_dtype = node["nodeDType"][0] + + if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: + return SOFIE.ROperator_Selu["float"](inputs[0], outputs[0]) + else: + raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Selu does not yet support input type " + node_dtype) + + +def MakePyTorchSigmoid(node): + """ + Create a SOFIE Sigmoid operator from a PyTorch ONNX Graph node. + + For instantiating a ROperator_Sigmoid object, the names of + input & output tensors and the data type of the node are extracted. + + Parameters: + node (dict): A dictionary containing node information including type, attributes, + input & output tensor names and the data type (must be float). + + Returns: + ROperator_Sigmoid: A SOFIE framework operator representing the Sigmoid operation. + """ + from ROOT.TMVA.Experimental import SOFIE + + inputs = node["nodeInputs"] + outputs = node["nodeOutputs"] + node_dtype = node["nodeDType"][0] + + if SOFIE.ConvertStringToType(node_dtype) == SOFIE.ETensorType.FLOAT: + return SOFIE.ROperator_Sigmoid["float"](inputs[0], outputs[0]) + else: + raise RuntimeError("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + node_dtype) + + +def MakePyTorchTranspose(node): + """ + Create a SOFIE Transpose operator from a PyTorch ONNX Graph node. + + For the Transpose operator of PyTorch's ONNX Graph, the permute dimensions are + found, and are passed in instantiating the ROperator object. + + Parameters: + node (dict): A dictionary containing node information including type, attributes + and input & output tensor names. + + Returns: + ROperator_Transpose: A SOFIE framework operator representing the Transpose operation. + """ + from ROOT.TMVA.Experimental import SOFIE + + attributes = node["nodeAttributes"] + inputs = node["nodeInputs"] + outputs = node["nodeOutputs"] + + # Extracting the permute dimensions for the transpose + attr_perm = [int(dim) for dim in attributes["perm"]] + + return SOFIE.ROperator_Transpose(attr_perm, inputs[0], outputs[0]) + + +# Set global dictionary, mapping PyTorch ONNX Graph nodes to corresponding functions +# that create their ROperator instances +mapPyTorchNode = { + "onnx::Gemm": MakePyTorchGemm, + "onnx::Conv": MakePyTorchConv, + "onnx::Relu": MakePyTorchRelu, + "onnx::Selu": MakePyTorchSelu, + "onnx::Sigmoid": MakePyTorchSigmoid, + "onnx::Transpose": MakePyTorchTranspose, +} + + +def MakePyTorchNode(node): + """ + Prepare the equivalent ROperator with respect to a PyTorch ONNX Graph node. + + The function searches for the passed PyTorch ONNX Graph node in the map, and calls + the specific preparatory function, subsequently returning the ROperator object. + + For developing new preparatory functions for supporting PyTorch ONNX Graph nodes + in the future, all one needs is to extract the required properties and attributes + from the node dictionary, which contains all the information about any PyTorch ONNX + Graph node, and after any required transformations, these are passed for instantiating + the ROperator object. + + The node dictionary which holds all the information about a PyTorch ONNX Graph's node + has the following structure: + + dict node { 'nodeType' : Type of node (operator) + 'nodeAttributes' : Attributes of the node + 'nodeInputs' : List of names of input tensors + 'nodeOutputs' : List of names of output tensors + 'nodeDType' : Data type of the operator node + } + + Parameters: + node (dict): A dictionary representing a PyTorch ONNX Graph node. + + Returns: + ROperator: A SOFIE framework operator representing the node operation. + """ + node_type = node["nodeType"] + if node_type not in mapPyTorchNode: + raise RuntimeError("TMVA::SOFIE - Parsing PyTorch node " + node_type + " is not yet supported") + return mapPyTorchNode[node_type](node) + + +class PyTorch: + def Parse(filename, input_shapes, input_dtypes=None): + """ + Parse a trained PyTorch .pt model into a RModel object. + + The parser uses internal functions of PyTorch to convert any PyTorch model + into its equivalent ONNX Graph. For this conversion, dummy inputs are built + which are passed through the model and the applied operators are recorded + for populating the ONNX graph. This requires the shapes and data types of + the input tensors, which are used for building the dummy inputs. + After the said conversion, the nodes of the ONNX graph are then traversed to + extract properties like node type, attributes and input & output tensor names, + and the equivalent ROperator instances are added into the RModel object. + + The internal function used to convert the model to a graph object returns a + list which contains a Graph object and a dictionary of weights. This dictionary + is used to add the initialized tensors of the model into the RModel object. + + For adding the input tensor infos, the names of the input tensors are extracted + from the PyTorch ONNX graph object. The vectors of shapes & data types passed + into the Parse() function are used for the shapes and the data types of the + input tensors. + + For the output tensor infos, the names of the output tensors are also extracted + from the Graph object and are then added into the RModel object. + + Parameters: + filename (str): File location of the PyTorch .pt model. + input_shapes: List of input shape lists. + input_dtypes: Optional list of SOFIE.ETensorType for the data types of the + input tensors. Defaults to float for all input tensors. + + Returns: + RModel: The parsed model. + + Example usage: + ~~~ {.py} + import ROOT + model = ROOT.TMVA.Experimental.SOFIE.PyTorch.Parse("trained_model_dense.pt", [[120, 1]]) + ~~~ + """ + + # PyTorch is too fragile to import unconditionally. As its presence might break several ROOT + # usecases and importing torch globally will slow down importing ROOT, which is not desired. + # For this, we import torch within the functions instead of importing it at the start of the + # file (i.e. globally). So, whenever the parser function is called, only then torch will be + # imported, and not everytime we import ROOT. Also, we can import torch in multiple functions + # as many times as we want since Python caches the imported packages. + + import torch + from ROOT.TMVA.Experimental import SOFIE + from torch.onnx.utils import _model_to_graph + + # Check if file exists + if not os.path.exists(filename): + raise RuntimeError("Model file {} not found!".format(filename)) + + # create new RModel object + sep = "/" + if os.name == "nt": + sep = "\\" + + isep = filename.rfind(sep) + filename_nodir = filename + if isep != -1: + filename_nodir = filename[isep + 1 :] + + ttime = time.time() + gmt_time = time.gmtime(ttime) + parsetime = time.asctime(gmt_time) + + rmodel = SOFIE.RModel(filename_nodir, parsetime) + + print("Torch Version: " + torch.__version__) + + # The data types of the input tensors default to float + if input_dtypes is None: + input_dtypes = [SOFIE.ETensorType.FLOAT] * len(input_shapes) + + # Load the model and prepare it for the conversion to an ONNX graph + model = torch.jit.load(filename) + model.cpu() + model.eval() + + # Build dummy inputs for the model from the given input shapes + dummy_inputs = [torch.rand(*[int(dim) for dim in shape]) for shape in input_shapes] + + # Get the ONNX graph from the model using the dummy inputs + graph = _model_to_graph(model, dummy_inputs) + + # Iterate over the nodes of the ONNX graph and add the equivalent operators to the RModel + for node in graph[0].nodes(): + node_data = {} + node_data["nodeType"] = node.kind() + node_data["nodeAttributes"] = {name: _node_get(node, name) for name in node.attributeNames()} + node_data["nodeInputs"] = [x.debugName() for x in node.inputs()] + node_data["nodeOutputs"] = [x.debugName() for x in node.outputs()] + node_data["nodeDType"] = [x.type().scalarType() for x in node.outputs()] + + # Adding required routines depending on the node types for generating inference code + node_type = node_data["nodeType"] + if node_type == "onnx::Gemm": + rmodel.AddBlasRoutines(["Gemm", "Gemv"]) + elif node_type == "onnx::Selu" or node_type == "onnx::Sigmoid": + rmodel.AddNeededStdLib("cmath") + elif node_type == "onnx::Conv": + rmodel.AddBlasRoutines(["Gemm", "Axpy"]) + + rmodel.AddOperator(move_operator(MakePyTorchNode(node_data))) + + # Extract the model weights to add the initialized tensors to the RModel + for weight_name, weight_tensor in graph[1].items(): + # e.g. "torch.FloatTensor" -> "Float" + weight_dtype = SOFIE.ConvertStringToType(weight_tensor.type()[6:-6]) + if weight_dtype == SOFIE.ETensorType.FLOAT: + weight_value = weight_tensor.numpy() + rmodel.AddInitializedTensor( + weight_name, SOFIE.ETensorType.FLOAT, list(weight_value.shape), weight_value.flatten() + ) + else: + raise RuntimeError( + "Type error: TMVA SOFIE does not yet supports weights of data type " + + SOFIE.ConvertTypeToString(weight_dtype) + ) + + # Extract the input tensor infos (the first graph input is the model itself) + input_names = [x.debugName() for x in model.graph.inputs()][1:] + for input_name, input_shape, input_dtype in zip(input_names, input_shapes, input_dtypes): + if input_dtype == SOFIE.ETensorType.FLOAT: + rmodel.AddInputTensorInfo(input_name, SOFIE.ETensorType.FLOAT, [int(dim) for dim in input_shape]) + rmodel.AddInputTensorName(input_name) + else: + raise RuntimeError( + "Type Error: TMVA SOFIE does not yet support the input tensor data type " + + SOFIE.ConvertTypeToString(input_dtype) + ) + + # Extract the output tensor names + output_names = [x.debugName() for x in graph[0].outputs()] + rmodel.AddOutputTensorNameList(output_names) + + return rmodel diff --git a/tmva/sofie/README.md b/tmva/sofie/README.md index 54592f47249de..79043be5c7bce 100644 --- a/tmva/sofie/README.md +++ b/tmva/sofie/README.md @@ -178,7 +178,7 @@ parser.CheckModel("example_model.ONNX"); - [TMVA_SOFIE_Keras](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_Keras.C) - [TMVA_SOFIE_Keras_HiggsModel](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_Keras_HiggsModel.C) - [TMVA_SOFIE_ONNX](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_ONNX.C) - - [TMVA_SOFIE_PyTorch](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_PyTorch.C) + - [TMVA_SOFIE_PyTorch](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_PyTorch.py) - [TMVA_SOFIE_RDataFrame](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.C) - [TMVA_SOFIE_RDataFrame](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_RDataFrame.py) - [TMVA_SOFIE_RDataFrame_JIT](https://github.com/root-project/root/blob/master/tutorials/machine_learning/TMVA_SOFIE_RDataFrame_JIT.C) diff --git a/tmva/sofie_parsers/CMakeLists.txt b/tmva/sofie_parsers/CMakeLists.txt index 73f9aaf0ad8d5..1a396432f73d2 100644 --- a/tmva/sofie_parsers/CMakeLists.txt +++ b/tmva/sofie_parsers/CMakeLists.txt @@ -21,8 +21,6 @@ set_source_files_properties(${PROTO_SRCS} PROPERTIES COMPILE_FLAGS -Wno-unused-p ROOT_STANDARD_LIBRARY_PACKAGE(ROOTTMVASofieParser HEADERS TMVA/RModelParser_ONNX.hxx - TMVA/RModelParser_Keras.h - TMVA/RModelParser_PyTorch.h SOURCES src/RModelParser_ONNX.cxx src/ParseBasicUnary.cxx @@ -89,15 +87,6 @@ ROOT_STANDARD_LIBRARY_PACKAGE(ROOTTMVASofieParser ROOTTMVASofie ) -# Separate library for the parses that have to link against libpython. -ROOT_LINKER_LIBRARY(ROOTTMVASofiePyParsers - src/RModelParser_Keras.cxx - src/RModelParser_PyTorch.cxx - LIBRARIES - Python3::Python - ROOTTMVASofie -) - target_include_directories(ROOTTMVASofieParser PUBLIC $) target_include_directories(ROOTTMVASofieParser BEFORE PUBLIC diff --git a/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h b/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h deleted file mode 100644 index ed658526065c9..0000000000000 --- a/tmva/sofie_parsers/inc/TMVA/RModelParser_Keras.h +++ /dev/null @@ -1,48 +0,0 @@ -// @(#)root/tmva/pymva $Id$ -// Author: Sanjiban Sengupta, 2021 - -/********************************************************************************** - * Project: TMVA - a Root-integrated toolkit for multivariate data analysis * - * Package: TMVA * - * * - * * - * Description: * - * Functionality for parsing a saved Keras .H5 model into RModel object * - * * - * Authors (alphabetical): * - * Sanjiban Sengupta * - * * - * Copyright (c) 2021: * - * CERN, Switzerland * - * * - * * - * Redistribution and use in source and binary forms, with or without * - * modification, are permitted according to the terms listed in LICENSE * - * (see tmva/doc/LICENSE) * - **********************************************************************************/ - - -#ifndef TMVA_SOFIE_RMODELPARSER_KERAS -#define TMVA_SOFIE_RMODELPARSER_KERAS - -#include "TMVA/RModel.hxx" -#include "TMVA/SOFIE_common.hxx" -#include "TMVA/Types.h" -#include "TMVA/OperatorList.hxx" - -#include "Rtypes.h" -#include "TString.h" - - -namespace TMVA::Experimental::SOFIE::PyKeras { - -/// Parser function for translating Keras .h5 model into a RModel object. -/// Accepts the file location of a Keras model and returns the -/// equivalent RModel object. -/// One can specify as option a batch size that can be used when the input Keras model -/// has not a defined input batch size : e.g. for input = (input_dim,) -RModel Parse(std::string filename, int batch_size = -1); - -} // namespace TMVA::Experimental::SOFIE::PyKeras - -#endif //TMVA_PYMVA_RMODELPARSER_KERAS diff --git a/tmva/sofie_parsers/inc/TMVA/RModelParser_PyTorch.h b/tmva/sofie_parsers/inc/TMVA/RModelParser_PyTorch.h deleted file mode 100644 index 3d02aa9010cb9..0000000000000 --- a/tmva/sofie_parsers/inc/TMVA/RModelParser_PyTorch.h +++ /dev/null @@ -1,52 +0,0 @@ -// @(#)root/tmva/pymva $Id$ -// Author: Sanjiban Sengupta, 2021 - -/********************************************************************************** - * Project: TMVA - a Root-integrated toolkit for multivariate data analysis * - * Package: TMVA * - * * - * * - * Description: * - * Functionality for parsing a saved PyTorch .PT model into RModel object * - * * - * Authors (alphabetical): * - * Sanjiban Sengupta * - * * - * Copyright (c) 2021: * - * CERN, Switzerland * - * * - * * - * Redistribution and use in source and binary forms, with or without * - * modification, are permitted according to the terms listed in LICENSE * - * (see tmva/doc/LICENSE) * - **********************************************************************************/ - - -#ifndef TMVA_SOFIE_RMODELPARSER_PYTORCH -#define TMVA_SOFIE_RMODELPARSER_PYTORCH - -#include "TMVA/RModel.hxx" -#include "TMVA/SOFIE_common.hxx" -#include "TMVA/Types.h" -#include "TMVA/OperatorList.hxx" - -#include "Rtypes.h" -#include "TString.h" - - -namespace TMVA::Experimental::SOFIE::PyTorch { - -/// Parser function for translating PyTorch .pt model into a RModel object. -/// Accepts the file location of a PyTorch model, shapes and data-types of input tensors -/// and returns the equivalent RModel object. -RModel Parse(std::string filepath, std::vector> inputShapes, std::vector dtype); - -/// Overloaded Parser function for translating PyTorch .pt model into a RModel object. -/// Accepts the file location of a PyTorch model and the shapes of input tensors. -/// Builds the vector of data-types for input tensors and calls the `Parse()` function to -/// return the equivalent RModel object. -RModel Parse(std::string filepath, std::vector> inputShapes); - -} // namespace TMVA::Experimental::SOFIE::PyTorch - -#endif //TMVA_PYMVA_RMODELPARSER_PYTORCH diff --git a/tmva/sofie_parsers/src/RModelParser_Keras.cxx b/tmva/sofie_parsers/src/RModelParser_Keras.cxx deleted file mode 100644 index 9f4d334b4cd37..0000000000000 --- a/tmva/sofie_parsers/src/RModelParser_Keras.cxx +++ /dev/null @@ -1,19 +0,0 @@ -// @(#)root/tmva/pymva $Id$ -// Author: Sanjiban Sengupta 2021 - - -#include "TMVA/RModelParser_Keras.h" - - -namespace TMVA::Experimental::SOFIE::PyKeras { - - - -RModel Parse(std::string /*filename*/, int /* batch_size */ ){ - - throw std::runtime_error("TMVA::SOFIE C++ Keras parser is deprecated. Use the python3 function " - "model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse('model.keras',batch_size=1) " ); - - return RModel(); -} -} \ No newline at end of file diff --git a/tmva/sofie_parsers/src/RModelParser_PyTorch.cxx b/tmva/sofie_parsers/src/RModelParser_PyTorch.cxx deleted file mode 100644 index b19ff5018d8e4..0000000000000 --- a/tmva/sofie_parsers/src/RModelParser_PyTorch.cxx +++ /dev/null @@ -1,582 +0,0 @@ -// @(#)root/tmva/pymva $Id$ -// Author: Sanjiban Sengupta 2021 - -/********************************************************************************** - * Project : TMVA - a Root-integrated toolkit for multivariate data analysis * - * Package : TMVA * - * Function: TMVA::Experimental::SOFIE::PyTorch::Parse * - * * - * Description: * - * Parser function for translating PyTorch .pt model to RModel object * - * * - * Example Usage: * - * ~~~ {.cpp} * - * using TMVA::Experimental::SOFIE; * - * // Building the vector of input tensor shapes * - * std::vector s1{120,1}; * - * std::vector> inputShape{s1}; * - * RModel model = PyTorch::Parse("trained_model_dense.pt",inputShape); * - * ~~~ * - * * - **********************************************************************************/ - - -#include "TMVA/RModelParser_PyTorch.h" - -#include - -namespace TMVA::Experimental::SOFIE::PyTorch { - -namespace { - -// Utility functions (taken from PyMethodBase in PyMVA) - -void PyRunString(TString code, PyObject *globalNS, PyObject *localNS) -{ - PyObject *fPyReturn = PyRun_String(code, Py_single_input, globalNS, localNS); - if (!fPyReturn) { - std::cout << "\nPython error message:\n"; - PyErr_Print(); - throw std::runtime_error("\nFailed to run python code: " + code); - } -} - -const char *PyStringAsString(PyObject *string) -{ - PyObject *encodedString = PyUnicode_AsUTF8String(string); - const char *cstring = PyBytes_AsString(encodedString); - return cstring; -} - -std::vector GetDataFromList(PyObject *listObject) -{ - std::vector listVec; - for (Py_ssize_t listIter = 0; listIter < PyList_Size(listObject); ++listIter) { - listVec.push_back((size_t)PyLong_AsLong(PyList_GetItem(listObject, listIter))); - } - return listVec; -} - -} // namespace - - -namespace INTERNAL{ - -// For searching and calling specific preparatory function for PyTorch ONNX Graph's node -std::unique_ptr MakePyTorchNode(PyObject* fNode); - -std::unique_ptr MakePyTorchGemm(PyObject* fNode); // For instantiating ROperator for PyTorch ONNX's Gemm operator -std::unique_ptr MakePyTorchConv(PyObject* fNode); // For instantiating ROperator for PyTorch ONNX's Conv operator -std::unique_ptr MakePyTorchRelu(PyObject* fNode); // For instantiating ROperator for PyTorch ONNX's Relu operator -std::unique_ptr MakePyTorchSelu(PyObject* fNode); // For instantiating ROperator for PyTorch ONNX's Selu operator -std::unique_ptr MakePyTorchSigmoid(PyObject* fNode); // For instantiating ROperator for PyTorch ONNX's Sigmoid operator -std::unique_ptr MakePyTorchTranspose(PyObject* fNode); // For instantiating ROperator for PyTorch ONNX's Transpose operator - -// For mapping PyTorch ONNX Graph's Node with the preparatory functions for ROperators -using PyTorchMethodMap = std::unordered_map (*)(PyObject* fNode)>; - -const PyTorchMethodMap mapPyTorchNode = -{ - {"onnx::Gemm", &MakePyTorchGemm}, - {"onnx::Conv", &MakePyTorchConv}, - {"onnx::Relu", &MakePyTorchRelu}, - {"onnx::Selu", &MakePyTorchSelu}, - {"onnx::Sigmoid", &MakePyTorchSigmoid}, - {"onnx::Transpose", &MakePyTorchTranspose} -}; - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares equivalent ROperator with respect to PyTorch ONNX node. -/// -/// \param[in] fNode Python PyTorch ONNX Graph node -/// \return unique pointer to ROperator object -/// -/// Function searches for the passed PyTorch ONNX Graph node in the map, and calls -/// the specific preparatory function, subsequently returning the ROperator object. -/// -/// For developing new preparatory functions for supporting PyTorch ONNX Graph nodes -/// in future, all one needs is to extract the required properties and attributes -/// from the fNode dictionary which contains all the information about any PyTorch ONNX -// Graph node and after any required transformations, these are passed for instantiating -/// the ROperator object. -/// -/// The fNode dictionary which holds all the information about a PyTorch ONNX Graph's node has -/// following structure:- -/// -/// dict fNode { 'nodeType' : Type of node (operator) -/// 'nodeAttributes' : Attributes of the node -/// 'nodeInputs' : List of names of input tensors -/// 'nodeOutputs' : List of names of output tensors -/// 'nodeDType' : Data-type of the operator node -/// } -/// -std::unique_ptr MakePyTorchNode(PyObject* fNode){ - std::string fNodeType = PyStringAsString(PyDict_GetItemString(fNode,"nodeType")); - auto findNode = mapPyTorchNode.find(fNodeType); - if(findNode == mapPyTorchNode.end()){ - throw std::runtime_error("TMVA::SOFIE - Parsing PyTorch node " +fNodeType+" is not yet supported "); - } - return (findNode->second)(fNode); -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator_Gemm object -/// -/// \param[in] fNode Python PyTorch ONNX Graph node -/// \return Unique pointer to ROperator object -/// -/// For PyTorch's Linear layer having Gemm operation in its ONNX graph, -/// the names of the input tensor, output tensor are extracted, and then -/// are passed to instantiate a ROperator_Gemm object using the required attributes. -/// fInputs is a list of tensor names, which includes the names of the input tensor -/// and the weight tensors. -std::unique_ptr MakePyTorchGemm(PyObject* fNode){ - PyObject* fAttributes = PyDict_GetItemString(fNode,"nodeAttributes"); - PyObject* fInputs = PyDict_GetItemString(fNode,"nodeInputs"); - PyObject* fOutputs = PyDict_GetItemString(fNode,"nodeOutputs"); - std::string fNodeDType = PyStringAsString(PyList_GetItem(PyDict_GetItemString(fNode,"nodeDType"),0)); - - // Extracting the parameters for Gemm Operator - std::string fNameA = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fNameB = PyStringAsString(PyList_GetItem(fInputs,1)); - std::string fNameC = PyStringAsString(PyList_GetItem(fInputs,2)); - std::string fNameY = PyStringAsString(PyList_GetItem(fOutputs,0)); - float fAttrAlpha = (float)(PyFloat_AsDouble(PyDict_GetItemString(fAttributes,"alpha"))); - float fAttrBeta = (float)(PyFloat_AsDouble(PyDict_GetItemString(fAttributes,"beta"))); - int_t fAttrTransA; - int_t fAttrTransB; - - if(PyDict_Contains(fAttributes,PyUnicode_FromString("transB"))){ - fAttrTransB = (int_t)(PyLong_AsLong(PyDict_GetItemString(fAttributes,"transB"))); - fAttrTransA = !fAttrTransB; - } - else{ - fAttrTransA=(int_t)(PyLong_AsLong(PyDict_GetItemString(fAttributes,"transA"))); - fAttrTransB = !fAttrTransA; - } - - std::unique_ptr op; - switch(ConvertStringToType(fNodeDType)){ - case ETensorType::FLOAT: { - op.reset(new ROperator_Gemm(fAttrAlpha, fAttrBeta, fAttrTransA, fAttrTransB, fNameA, fNameB, fNameC, fNameY )); - break; - } - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Gemm does not yet support input type " + fNodeDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator_Relu object -/// -/// \param[in] fNode Python PyTorch ONNX Graph node -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Relu object, the names of -/// input & output tensors and the data-type of the Graph node -/// are extracted. -std::unique_ptr MakePyTorchRelu(PyObject* fNode){ - PyObject* fInputs = PyDict_GetItemString(fNode,"nodeInputs"); - PyObject* fOutputs = PyDict_GetItemString(fNode,"nodeOutputs"); - std::string fNodeDType = PyStringAsString(PyList_GetItem(PyDict_GetItemString(fNode,"nodeDType"),0)); - std::string fNameX = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string fNameY = PyStringAsString(PyList_GetItem(fOutputs,0)); - std::unique_ptr op; - switch(ConvertStringToType(fNodeDType)){ - case ETensorType::FLOAT: { - op.reset(new ROperator_Relu(fNameX,fNameY)); - break; - } - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Relu does not yet support input type " + fNodeDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator_Selu object -/// -/// \param[in] fNode Python PyTorch ONNX Graph node -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Selu object, the names of -/// input & output tensors and the data-type of the Graph node -/// are extracted. -std::unique_ptr MakePyTorchSelu(PyObject* fNode){ - PyObject* fInputs = PyDict_GetItemString(fNode,"nodeInputs"); - PyObject* fOutputs = PyDict_GetItemString(fNode,"nodeOutputs"); - std::string fNodeDType = PyStringAsString(PyList_GetItem(PyDict_GetItemString(fNode,"nodeDType"),0)); - - std::unique_ptr op; - switch(ConvertStringToType(fNodeDType)){ - case ETensorType::FLOAT: { - op.reset(new ROperator_Selu(PyStringAsString(PyList_GetItem(fInputs,0)), PyStringAsString(PyList_GetItem(fOutputs,0)))); - break; - } - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Selu does not yet support input type " + fNodeDType); - } - return op; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator_Sigmoid object -/// -/// \param[in] fNode Python PyTorch ONNX Graph node -/// \return Unique pointer to ROperator object -/// -/// For instantiating a ROperator_Sigmoid object, the names of -/// input & output tensors and the data-type of the Graph node -/// are extracted. -std::unique_ptr MakePyTorchSigmoid(PyObject* fNode){ - PyObject* fInputs = PyDict_GetItemString(fNode,"nodeInputs"); - PyObject* fOutputs = PyDict_GetItemString(fNode,"nodeOutputs"); - std::string fNodeDType = PyStringAsString(PyList_GetItem(PyDict_GetItemString(fNode,"nodeDType"),0)); - - std::unique_ptr op; - switch(ConvertStringToType(fNodeDType)){ - case ETensorType::FLOAT: { - op.reset(new ROperator_Sigmoid(PyStringAsString(PyList_GetItem(fInputs,0)), PyStringAsString(PyList_GetItem(fOutputs,0)))); - break; - } - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Sigmoid does not yet support input type " + fNodeDType); - } - return op; -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator_Transpose object -/// -/// \param[in] fNode Python PyTorch ONNX Graph node -/// \return Unique pointer to ROperator object -/// -/// For Transpose Operator of PyTorch's ONNX Graph, the permute dimensions are found, -/// and are passed in instantiating the ROperator object. -std::unique_ptr MakePyTorchTranspose(PyObject* fNode){ - PyObject* fAttributes = PyDict_GetItemString(fNode,"nodeAttributes"); - PyObject* fInputs = PyDict_GetItemString(fNode,"nodeInputs"); - PyObject* fOutputs = PyDict_GetItemString(fNode,"nodeOutputs"); - std::string fNodeDType = PyStringAsString(PyList_GetItem(PyDict_GetItemString(fNode,"nodeDType"),0)); - - // Extracting the Permute dimensions for transpose - std::vector fAttrPermute; - PyObject* fPermute=PyDict_GetItemString(fAttributes,"perm"); - for(Py_ssize_t permIter=0; permIter op = std::make_unique(fAttrPermute, fNameData, fNameOutput); - return op; -} - - -////////////////////////////////////////////////////////////////////////////////// -/// \brief Prepares a ROperator_Conv object -/// -/// \param[in] fNode Python PyTorch ONNX Graph node -/// \return Unique pointer to ROperator object -/// -/// For Conv Operator of PyTorch's ONNX Graph, attributes like dilations, group, -/// kernel shape, pads and strides are found, and are passed in instantiating the -/// ROperator object with autopad default to `NOTSET`. -std::unique_ptr MakePyTorchConv(PyObject* fNode){ - PyObject* fAttributes = PyDict_GetItemString(fNode,"nodeAttributes"); - PyObject* fInputs = PyDict_GetItemString(fNode,"nodeInputs"); - PyObject* fOutputs = PyDict_GetItemString(fNode,"nodeOutputs"); - std::string fNodeDType = PyStringAsString(PyList_GetItem(PyDict_GetItemString(fNode,"nodeDType"),0)); - - // Extracting the Conv Node Attributes - PyObject* fDilations = PyDict_GetItemString(fAttributes,"dilations"); - PyObject* fGroup = PyDict_GetItemString(fAttributes,"group"); - PyObject* fKernelShape = PyDict_GetItemString(fAttributes,"kernel_shape"); - PyObject* fPads = PyDict_GetItemString(fAttributes,"pads"); - PyObject* fStrides = PyDict_GetItemString(fAttributes,"strides"); - - std::string fAttrAutopad = "NOTSET"; - std::vector fAttrDilations = GetDataFromList(fDilations); - size_t fAttrGroup = PyLong_AsLong(fGroup); - std::vector fAttrKernelShape = GetDataFromList(fKernelShape); - std::vector fAttrPads = GetDataFromList(fPads); - std::vector fAttrStrides = GetDataFromList(fStrides); - std::string nameX = PyStringAsString(PyList_GetItem(fInputs,0)); - std::string nameW = PyStringAsString(PyList_GetItem(fInputs,1)); - std::string nameB = PyStringAsString(PyList_GetItem(fInputs,2)); - std::string nameY = PyStringAsString(PyList_GetItem(fOutputs,0)); - - std::unique_ptr op; - switch(ConvertStringToType(fNodeDType)){ - case ETensorType::FLOAT: { - op.reset(new ROperator_Conv(fAttrAutopad, fAttrDilations, fAttrGroup, fAttrKernelShape, fAttrPads, fAttrStrides, nameX, nameW, nameB, nameY)); - break; - } - default: - throw std::runtime_error("TMVA::SOFIE - Unsupported - Operator Conv does not yet support input type " + fNodeDType); - } - return op; -} -}//INTERNAL - - -////////////////////////////////////////////////////////////////////////////////// -/// \param[in] filename file location of PyTorch .pt model -/// \param[in] inputShapes vector of input shape vectors -/// \param[in] inputDTypes vector of ETensorType for data-types of Input tensors -/// \return Parsed RModel object -/// -/// The `Parse()` function defined in `TMVA::Experimental::SOFIE::PyTorch` will -/// parse a trained PyTorch .pt model into a RModel Object. The parser uses -/// internal functions of PyTorch to convert any PyTorch model into its -/// equivalent ONNX Graph. For this conversion, dummy inputs are built which are -/// passed through the model and the applied operators are recorded for populating -/// the ONNX graph. The `Parse()` function requires the shapes and data-types of -/// the input tensors which are used for building the dummy inputs. -/// After the said conversion, the nodes of the ONNX graph are then traversed to -/// extract properties like Node type, Attributes, input & output tensor names. -/// Function `AddOperator()` is then called on the extracted nodes to add the -/// operator into the RModel object. The nodes are also checked for adding any -/// required routines for executing the generated Inference code. -/// -/// The internal function used to convert the model to graph object returns a list -/// which contains a Graph object and a dictionary of weights. This dictionary is -/// used to extract the Initialized tensors for the model. The names and data-types -/// of the Initialized tensors are extracted along with their values in NumPy array, -/// and after approapriate type-conversions, they are added into the RModel object. -/// -/// For adding the Input tensor infos, the names of the input tensors are extracted -/// from the PyTorch ONNX graph object. The vector of shapes & data-types passed -/// into the `Parse()` function are used to extract the data-type and the shape -/// of the input tensors. Extracted input tensor infos are then added into the -/// RModel object by calling the `AddInputTensorInfo()` function. -/// -/// For the output tensor infos, names of the output tensors are also extracted -/// from the Graph object and are then added into the RModel object by calling the -/// AddOutputTensorNameList() function. -/// -/// Example Usage: -/// ~~~ {.cpp} -/// using TMVA::Experimental::SOFIE; -/// //Building the vector of input tensor shapes -/// std::vector s1{120,1}; -/// std::vector> inputShape{s1}; -/// RModel model = PyTorch::Parse("trained_model_dense.pt",inputShape); -/// ~~~ -RModel Parse(std::string filename, std::vector> inputShapes, std::vector inputDTypes){ - - char sep = '/'; - #ifdef _WIN32 - sep = '\\'; - #endif - - size_t isep = filename.rfind(sep, filename.length()); - std::string filename_nodir = filename; - if (isep != std::string::npos){ - filename_nodir = (filename.substr(isep+1, filename.length() - isep)); - } - - //Check on whether the PyTorch .pt file exists - if(!std::ifstream(filename).good()){ - throw std::runtime_error("Model file "+filename_nodir+" not found!"); - } - - - std::time_t ttime = std::time(0); - std::tm* gmt_time = std::gmtime(&ttime); - std::string parsetime (std::asctime(gmt_time)); - - RModel rmodel(filename_nodir, parsetime); - - //Intializing Python Interpreter and scope dictionaries - Py_Initialize(); - PyObject* main = PyImport_AddModule("__main__"); - PyObject* fGlobalNS = PyModule_GetDict(main); - PyObject* fLocalNS = PyDict_New(); - if (!fGlobalNS) { - throw std::runtime_error("Can't init global namespace for Python"); - } - if (!fLocalNS) { - throw std::runtime_error("Can't init local namespace for Python"); - } - - - //Extracting model information - //Model is converted to ONNX graph format - //using PyTorch's internal function with the input shape provided - PyRunString("import torch",fGlobalNS,fLocalNS); - PyRunString("print('Torch Version: '+torch.__version__)",fGlobalNS,fLocalNS); - PyRunString("from torch.onnx.utils import _model_to_graph",fGlobalNS,fLocalNS); - //PyRunString("from torch.onnx.symbolic_helper import _set_onnx_shape_inference",fGlobalNS,fLocalNS); - PyRunString(TString::Format("model= torch.jit.load('%s')",filename.c_str()),fGlobalNS,fLocalNS); - PyRunString("globals().update(locals())",fGlobalNS,fLocalNS); - PyRunString("model.cpu()",fGlobalNS,fLocalNS); - PyRunString("model.eval()",fGlobalNS,fLocalNS); - - //Building dummy inputs for the model - PyRunString("dummyInputs=[]",fGlobalNS,fLocalNS); - for(long unsigned int it=0;it fWeightShape; - std::size_t fWeightSize; - - for(Py_ssize_t weightIter=0; weightIter fData(malloc(fWeightSize * sizeof(float)), free); - std::memcpy(fData.get(),fWeightValue,fWeightSize * sizeof(float)); - rmodel.AddInitializedTensor(fWeightName, ETensorType::FLOAT,fWeightShape,fData); - break; - } - default: - throw std::runtime_error("Type error: TMVA SOFIE does not yet supports weights of data type"+ConvertTypeToString(fWeightDType)); - } - } - - - //Extracting Input tensor info - PyRunString("inputs=[x for x in model.graph.inputs()]",fGlobalNS,fLocalNS); - PyRunString("inputs=inputs[1:]",fGlobalNS,fLocalNS); - PyRunString("inputNames=[x.debugName() for x in inputs]",fGlobalNS,fLocalNS); - PyObject* fPInputs= PyDict_GetItemString(fLocalNS,"inputNames"); - std::string fInputName; - std::vectorfInputShape; - ETensorType fInputDType; - for(Py_ssize_t inputIter=0; inputIter fOutputNames; - for(Py_ssize_t outputIter = 0; outputIter < PyList_Size(fPOutputs);++outputIter){ - fOutputNames.push_back(PyStringAsString(PyList_GetItem(fPOutputs,outputIter))); - } - rmodel.AddOutputTensorNameList(fOutputNames); - - return rmodel; -} - -////////////////////////////////////////////////////////////////////////////////// -/// \param[in] filepath file location of PyTorch .pt model -/// \param[in] inputShapes vector of input shape vectors -/// \return Parsed RModel object -/// -/// Overloaded Parser function for translating PyTorch .pt model to RModel object. -/// Function only requires the inputShapes vector as a parameter. Function -/// builds the vector of Data-types for the input tensors using Float as default, -/// Function calls the `Parse()` function with the vector of data-types included, -/// subsequently returning the parsed RModel object. -RModel Parse(std::string filepath,std::vector> inputShapes){ - std::vector dtype(inputShapes.size(),ETensorType::FLOAT); - return Parse(filepath,inputShapes,dtype); -} - -} // namespace TMVA::Experimental::SOFIE::PyTorch diff --git a/tmva/tmva/inc/TMVA/RSofieReader.hxx b/tmva/tmva/inc/TMVA/RSofieReader.hxx index c5d0c07052dc7..41a9c7c1749ff 100644 --- a/tmva/tmva/inc/TMVA/RSofieReader.hxx +++ b/tmva/tmva/inc/TMVA/RSofieReader.hxx @@ -120,26 +120,25 @@ public: parserPythonCode += "model = ROOT.TMVA.Experimental.SOFIE.PyKeras.Parse('" + path + "'," + batch_size + ")\n"; } else if (type == kPt) { - // use PyTorch direct parser - if (gSystem->Load("libROOTTMVASofiePyParsers") < 0) { - throw std::runtime_error("RSofieReader: cannot use SOFIE with PyTorch since libROOTTMVASofiePyParsers is missing"); - } + // use PyTorch Python parser if (inputShapes.size() == 0) { throw std::runtime_error("RSofieReader: cannot use SOFIE with PyTorch since the input tensor shape is missing and is needed by the PyTorch parser"); } - std::string inputShapesStr = "{"; + std::string inputShapesStr = "["; for (unsigned int i = 0; i < inputShapes.size(); i++) { - inputShapesStr += "{ "; + inputShapesStr += "["; for (unsigned int j = 0; j < inputShapes[i].size(); j++) { inputShapesStr += ROOT::Math::Util::ToString(inputShapes[i][j]); if (j < inputShapes[i].size()-1) inputShapesStr += ", "; } - inputShapesStr += "}"; + inputShapesStr += "]"; if (i < inputShapes.size()-1) inputShapesStr += ", "; } - inputShapesStr += "}"; - parserCode += "{\nTMVA::Experimental::SOFIE::RModel model = TMVA::Experimental::SOFIE::PyTorch::Parse(\"" + path + "\", " - + inputShapesStr + "); \n"; + inputShapesStr += "]"; + parserPythonCode += "\"\"\"\n"; + parserPythonCode += "import ROOT\n"; + parserPythonCode += + "model = ROOT.TMVA.Experimental.SOFIE.PyTorch.Parse('" + path + "', " + inputShapesStr + ")\n"; } else if (type == kROOT) { // use parser from ROOT @@ -155,7 +154,8 @@ public: // add custom operators if needed if (fCustomOperators.size() > 0) { if (!parserPythonCode.empty()) - throw std::runtime_error("Cannot use Custom operator with a Python parser (e.g. from a Keras model)"); + throw std::runtime_error( + "Cannot use Custom operator with a Python parser (e.g. from a Keras or PyTorch model)"); for (auto & op : fCustomOperators) { parserCode += "{ auto p = new TMVA::Experimental::SOFIE::ROperator_Custom(\"" diff --git a/tutorials/CMakeLists.txt b/tutorials/CMakeLists.txt index 74e4b713e102c..3297527a27e8f 100644 --- a/tutorials/CMakeLists.txt +++ b/tutorials/CMakeLists.txt @@ -373,7 +373,7 @@ else() list(APPEND tmva_veto machine_learning/TMVA_SOFIE_Inference.py) endif() if (NOT tmva-sofie OR NOT ROOT_TORCH_FOUND) - list(APPEND tmva_veto machine_learning/TMVA_SOFIE_PyTorch.C) + list(APPEND tmva_veto machine_learning/TMVA_SOFIE_PyTorch.py) endif() if (NOT tmva-sofie OR NOT ROOT_TORCH_FOUND OR NOT ROOT_ONNX_FOUND) list(APPEND tmva_veto machine_learning/TMVA_SOFIE_ONNX.py) @@ -943,6 +943,7 @@ if(pyroot) machine_learning/ml_dataloader_PyTorch.py machine_learning/ml_dataloader_resampling.py machine_learning/ml_dataloader_Higgs_Classification.py + machine_learning/TMVA_SOFIE_PyTorch.py ) file(GLOB requires_xgboost RELATIVE ${CMAKE_CURRENT_SOURCE_DIR} machine_learning/tmva101_Training.py diff --git a/tutorials/machine_learning/TMVA_SOFIE_PyTorch.C b/tutorials/machine_learning/TMVA_SOFIE_PyTorch.C deleted file mode 100644 index 1eb5ae9d74b80..0000000000000 --- a/tutorials/machine_learning/TMVA_SOFIE_PyTorch.C +++ /dev/null @@ -1,90 +0,0 @@ -/// \file -/// \ingroup tutorial_ml -/// \notebook -nodraw -/// This macro provides a simple example for the parsing of PyTorch .pt file -/// into RModel object and further generating the .hxx header files for inference. -/// -/// \macro_code -/// \macro_output -/// \author Sanjiban Sengupta - -using namespace TMVA::Experimental; - -TString pythonSrc = "\ -import torch\n\ -import torch.nn as nn\n\ -\n\ -model = nn.Sequential(\n\ - nn.Linear(32,16),\n\ - nn.ReLU(),\n\ - nn.Linear(16,8),\n\ - nn.ReLU()\n\ - )\n\ -\n\ -criterion = nn.MSELoss()\n\ -optimizer = torch.optim.SGD(model.parameters(),lr=0.01)\n\ -\n\ -x=torch.randn(2,32)\n\ -y=torch.randn(2,8)\n\ -\n\ -for i in range(500):\n\ - y_pred = model(x)\n\ - loss = criterion(y_pred,y)\n\ - optimizer.zero_grad()\n\ - loss.backward()\n\ - optimizer.step()\n\ -\n\ -model.eval()\n\ -m = torch.jit.script(model)\n\ -torch.jit.save(m,'PyTorchModel.pt')\n"; - - -void TMVA_SOFIE_PyTorch(){ - - // Running the Python script to generate PyTorch .pt file - - TMacro m; - m.AddLine(pythonSrc); - m.SaveSource("make_pytorch_model.py"); - gSystem->Exec("python3 make_pytorch_model.py"); - - // Parsing a PyTorch model requires the shape and data-type of input tensor - // Data-type of input tensor defaults to Float if not specified - std::vector inputTensorShapeSequential{2, 32}; - std::vector> inputShapesSequential{inputTensorShapeSequential}; - - // Parsing the saved PyTorch .pt file into RModel object - SOFIE::RModel model = SOFIE::PyTorch::Parse("PyTorchModel.pt", inputShapesSequential); - - // Generating inference code - model.Generate(); - model.OutputGenerated("PyTorchModel.hxx"); - - // Printing required input tensors - std::cout << "\n\n"; - model.PrintRequiredInputTensors(); - - // Printing initialized tensors (weights) - std::cout << "\n\n"; - model.PrintInitializedTensors(); - - // Printing intermediate tensors - std::cout << "\n\n"; - model.PrintIntermediateTensors(); - - // Checking if tensor already exist in model - std::cout << "\n\nTensor \"0weight\" already exist: " << std::boolalpha << model.CheckIfTensorAlreadyExist("0weight") - << "\n\n"; - std::vector tensorShape = model.GetTensorShape("0weight"); - std::cout << "Shape of tensor \"0weight\": "; - for (auto &it : tensorShape) { - std::cout << it << ","; - } - std::cout<<"\n\nData type of tensor \"0weight\": "; - SOFIE::ETensorType tensorType = model.GetTensorType("0weight"); - std::cout<"]() +input_shapes.push_back([2, 32]) + +# Parsing the saved PyTorch .pt file into RModel object +model = SOFIE.PyTorch.Parse("PyTorchModel.pt", input_shapes) + +# Generating inference code +model.Generate() +model.OutputGenerated("PyTorchModel.hxx") + +# Printing required input tensors +print("\n") +model.PrintRequiredInputTensors() + +# Printing initialized tensors (weights) +print("\n") +model.PrintInitializedTensors() + +# Printing intermediate tensors +print("\n") +model.PrintIntermediateTensors() + +# Checking if tensor already exist in model +tensor_exists = bool(model.CheckIfTensorAlreadyExist("0weight")) +print(f'\n\nTensor "0weight" already exist: {str(tensor_exists).lower()}\n') + +tensor_shape = model.GetTensorShape("0weight") +print('Shape of tensor "0weight": ' + ",".join(str(dim) for dim in tensor_shape) + ",") + +tensor_type = model.GetTensorType("0weight") +print(f'\nData type of tensor "0weight": {SOFIE.ConvertTypeToString(tensor_type)}') + +# Printing generated inference code +print() +model.PrintGenerated() diff --git a/tutorials/machine_learning/index.md b/tutorials/machine_learning/index.md index b944683d97153..c97e782e8ac44 100644 --- a/tutorials/machine_learning/index.md +++ b/tutorials/machine_learning/index.md @@ -124,7 +124,7 @@ | TMVA_SOFIE_Keras_HiggsModel.C | | Run the SOFIE parser on the Keras model obtaining running TMVA_Higgs_Classification.C. You need to run that macro before this one. | | | TMVA_SOFIE_Models.py | Inference with SOFIE using a set of models trained with Keras. | | TMVA_SOFIE_ONNX.C | | Parsing of ONNX files into RModel object and further generating the .hxx header files for inference. | -| TMVA_SOFIE_PyTorch.C | | Parsing of PyTorch .pt file into RModel object and further generating the .hxx header files for inference. | +| | TMVA_SOFIE_PyTorch.py | Parsing of PyTorch .pt file into RModel object and further generating the .hxx header files for inference. | | TMVA_SOFIE_RDataFrame.C | TMVA_SOFIE_RDataFrame.py | Inference with SOFIE and RDataFrame, of a model trained with Keras. | | TMVA_SOFIE_RDataFrame_JIT.C | | Using a trained model with Keras and make inference using SOFIE and RDataFrame. | | TMVA_SOFIE_RSofieReader.C | | Using a trained model with Keras and make inference using SOFIE with the RSofieReader class. |