Skip to content

Latest commit

 

History

History
793 lines (588 loc) · 41.8 KB

torch_script_custom_ops.md

File metadata and controls

793 lines (588 loc) · 41.8 KB

Extending TorchScript with Custom C++ Operators

The PyTorch 1.0 release introduced a new programming model to PyTorch called TorchScript. TorchScript is a subset of the Python programming language which can be parsed, compiled and optimized by the TorchScript compiler. Further, compiled TorchScript models have the option of being serialized into an on-disk file format, which you can subsequently load and run from pure C++ (as well as Python) for inference.

TorchScript supports a large subset of operations provided by the torch package, allowing you to express many kinds of complex models purely as a series of tensor operations from PyTorch’s “standard library”. Nevertheless, there may be times where you find yourself in need of extending TorchScript with a custom C++ or CUDA function. While we recommend that you only resort to this option if your idea cannot be expressed (efficiently enough) as a simple Python function, we do provide a very friendly and simple interface for defining custom C++ and CUDA kernels using ATen, PyTorch’s high performance C++ tensor library. Once bound into TorchScript, you can embed these custom kernels (or “ops”) into your TorchScript model and execute them both in Python and in their serialized form directly in C++.

The following paragraphs give an example of writing a TorchScript custom op to call into OpenCV, a computer vision library written in C++. We will discuss how to work with tensors in C++, how to efficiently convert them to third party tensor formats (in this case, OpenCV [](#id1)Mats), how to register your operator with the TorchScript runtime and finally how to compile the operator and use it in Python and C++.

This tutorial assumes you have the preview release of PyTorch 1.0 installed via pip or <cite>conda</cite>. See https://pytorch.org/get-started/locally for instructions on grabbing the latest release of PyTorch 1.0. Alternatively, you can compile PyTorch from source. The documentation in this file will assist you with this.

Implementing the Custom Operator in C++

For this tutorial, we’ll be exposing the warpPerspective function, which applies a perspective transformation to an image, from OpenCV to TorchScript as a custom operator. The first step is to write the implementation of our custom operator in C++. Let’s call the file for this implementation op.cpp and make it look like this:

#include <opencv2/opencv.hpp>
#include <torch/script.h>

torch::Tensor warp_perspective(torch::Tensor image, torch::Tensor warp) {
  cv::Mat image_mat(/*rows=*/image.size(0),
                    /*cols=*/image.size(1),
                    /*type=*/CV_32FC1,
                    /*data=*/image.data<float>());
  cv::Mat warp_mat(/*rows=*/warp.size(0),
                   /*cols=*/warp.size(1),
                   /*type=*/CV_32FC1,
                   /*data=*/warp.data<float>());

  cv::Mat output_mat;
  cv::warpPerspective(image_mat, output_mat, warp_mat, /*dsize=*/{8, 8});

  torch::Tensor output = torch::from_blob(output_mat.ptr<float>(), /*sizes=*/{8, 8});
  return output.clone();
}

The code for this operator is quite short. At the top of the file, we include the OpenCV header file, opencv2/opencv.hpp, alongside the torch/script.h header which exposes all the necessary goodies from PyTorch’s C++ API that we need to write custom TorchScript operators. Our function warp_perspective takes two arguments: an input image and the warp transformation matrix we wish to apply to the image. The type of these inputs is torch::Tensor, PyTorch’s tensor type in C++ (which is also the underlying type of all tensors in Python). The return type of our warp_perspective function will also be a torch::Tensor.

Tip

See this note for more information about ATen, the library that provides the Tensor class to PyTorch. Further, this tutorial describes how to allocate and initialize new tensor objects in C++ (not required for this operator).

Attention

The TorchScript compiler understands a fixed number of types. Only these types can be used as arguments to your custom operator. Currently these types are: torch::Tensor, torch::Scalar, double, int64_t and std::vector``s of these types. Note that __only__ ``double and not float, and only int64_t and not other integral types such as int, short or long are supported.

Inside of our function, the first thing we need to do is convert our PyTorch tensors to OpenCV matrices, as OpenCV’s warpPerspective expects cv::Mat objects as inputs. Fortunately, there is a way to do this without copying any data. In the first few lines,

cv::Mat image_mat(/*rows=*/image.size(0),
                  /*cols=*/image.size(1),
                  /*type=*/CV_32FC1,
                  /*data=*/image.data<float>());

we are calling this constructor of the OpenCV Mat class to convert our tensor to a Mat object. We pass it the number of rows and columns of the original image tensor, the datatype (which we’ll fix as float32 for this example), and finally a raw pointer to the underlying data – a float*. What is special about this constructor of the Mat class is that it does not copy the input data. Instead, it will simply reference this memory for all operations performed on the Mat. If an in-place operation is performed on the image_mat, this will be reflected in the original image tensor (and vice-versa). This allows us to call subsequent OpenCV routines with the library’s native matrix type, even though we’re actually storing the data in a PyTorch tensor. We repeat this procedure to convert the warp PyTorch tensor to the warp_mat OpenCV matrix:

cv::Mat warp_mat(/*rows=*/warp.size(0),
                 /*cols=*/warp.size(1),
                 /*type=*/CV_32FC1,
                 /*data=*/warp.data<float>());

Next, we are ready to call the OpenCV function we were so eager to use in TorchScript: warpPerspective. For this, we pass the OpenCV function the image_mat and warp_mat matrices, as well as an empty output matrix called output_mat. We also specify the size dsize we want the output matrix (image) to be. It is hardcoded to 8 x 8 for this example:

cv::Mat output_mat;
cv::warpPerspective(image_mat, output_mat, warp_mat, /*dsize=*/{8, 8});

The final step in our custom operator implementation is to convert the output_mat back into a PyTorch tensor, so that we can further use it in PyTorch. This is strikingly similar to what we did earlier to convert in the other direction. In this case, PyTorch provides a torch::from_blob method. A blob in this case is intended to mean some opaque, flat pointer to memory that we want to interpret as a PyTorch tensor. The call to torch::from_blob looks like this:

torch::from_blob(output_mat.ptr<float>(), /*sizes=*/{8, 8})

We use the .ptr&lt;float&gt;() method on the OpenCV Mat class to get a raw pointer to the underlying data (just like .data&lt;float&gt;() for the PyTorch tensor earlier). We also specify the output shape of the tensor, which we hardcoded as 8 x 8. The output of torch::from_blob is then a torch::Tensor, pointing to the memory owned by the OpenCV matrix.

Before returning this tensor from our operator implementation, we must call .clone() on the tensor to perform a memory copy of the underlying data. The reason for this is that torch::from_blob returns a tensor that does not own its data. At that point, the data is still owned by the OpenCV matrix. However, this OpenCV matrix will go out of scope and be deallocated at the end of the function. If we returned the output tensor as-is, it would point to invalid memory by the time we use it outside the function. Calling .clone() returns a new tensor with a copy of the original data that the new tensor owns itself. It is thus safe to return to the outside world.

Registering the Custom Operator with TorchScript

Now that have implemented our custom operator in C++, we need to register it with the TorchScript runtime and compiler. This will allow the TorchScript compiler to resolve references to our custom operator in TorchScript code. Registration is very simple. For our case, we need to write:

static auto registry =
  torch::jit::RegisterOperators("my_ops::warp_perspective", &warp_perspective);

somewhere in the global scope of our op.cpp file. This creates a global variable registry, which will register our operator with TorchScript in its constructor (i.e. exactly once per program). We specify the name of the operator, and a pointer to its implementation (the function we wrote earlier). The name consists of two parts: a namespace (my_ops) and a name for the particular operator we are registering (warp_perspective). The namespace and operator name are separated by two colons (::).

Tip

If you want to register more than one operator, you can chain calls to .op() after the constructor:

static auto registry =
  torch::jit::RegisterOperators("my_ops::warp_perspective", &warp_perspective)
  .op("my_ops::another_op", &another_op)
  .op("my_ops::and_another_op", &and_another_op);

Behind the scenes, RegisterOperators will perform a number of fairly complicated C++ template metaprogramming magic tricks to infer the argument and return value types of the function pointer we pass it (&warp_perspective). This information is used to form a function schema for our operator. A function schema is a structured representation of an operator – a kind of “signature” or “prototype” – used by the TorchScript compiler to verify correctness in TorchScript programs.

Building the Custom Operator

Now that we have implemented our custom operator in C++ and written its registration code, it is time to build the operator into a (shared) library that we can load into Python for research and experimentation, or into C++ for inference in a no-Python environment. There exist multiple ways to build our operator, using either pure CMake, or Python alternatives like setuptools. For brevity, the paragraphs below only discuss the CMake approach. The appendix of this tutorial dives into the Python based alternatives.

Building with CMake

To build our custom operator into a shared library using the CMake build system, we need to write a short CMakeLists.txt file and place it with our previous op.cpp file. For this, let’s agree on a a directory structure that looks like this:

warp-perspective/
  op.cpp
  CMakeLists.txt

Also, make sure to grab the latest version of the LibTorch distribution, which packages PyTorch’s C++ libraries and CMake build files, from pytorch.org. Place the unzipped distribution somewhere accessible in your file system. The following paragraphs will refer to that location as /path/to/libtorch. The contents of our CMakeLists.txt file should then be the following:

cmake_minimum_required(VERSION 3.1 FATAL_ERROR)
project(warp_perspective)

find_package(Torch REQUIRED)
find_package(OpenCV REQUIRED)

# Define our library target
add_library(warp_perspective SHARED op.cpp)
# Enable C++11
target_compile_features(warp_perspective PRIVATE cxx_range_for)
# Link against LibTorch
target_link_libraries(warp_perspective "${TORCH_LIBRARIES}")
# Link against OpenCV
target_link_libraries(warp_perspective opencv_core opencv_imgproc)

Warning

This setup makes some assumptions about the build environment, particularly what pertains to the installation of OpenCV. The above CMakeLists.txt file was tested inside a Docker container running Ubuntu Xenial with libopencv-dev installed via apt. If it does not work for you and you feel stuck, please use the Dockerfile in the accompanying tutorial repository to build an isolated, reproducible environment in which to play around with the code from this tutorial. If you run into further troubles, please file an issue in the tutorial repository or post a question in our forum.

To now build our operator, we can run the following commands from our warp_perspective folder:

$ mkdir build
$ cd build
$ cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Looking for pthread.h
-- Looking for pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Found torch: /libtorch/lib/libtorch.so
-- Configuring done
-- Generating done
-- Build files have been written to: /warp_perspective/build
$ make -j
Scanning dependencies of target warp_perspective
[ 50%] Building CXX object CMakeFiles/warp_perspective.dir/op.cpp.o
[100%] Linking CXX shared library libwarp_perspective.so
[100%] Built target warp_perspective

which will place a libwarp_perspective.so shared library file in the build folder. In the cmake command above, you should replace /path/to/libtorch with the path to your unzipped LibTorch distribution.

We will explore how to use and call our operator in detail further below, but to get an early sensation of success, we can try running the following code in Python:

>>> import torch
>>> torch.ops.load_library("/path/to/libwarp_perspective.so")
>>> print(torch.ops.my_ops.warp_perspective)

Here, /path/to/libwarp_perspective.so should be a relative or absolute path to the libwarp_perspective.so shared library we just built. If all goes well, this should print something like

<built-in method my_ops::warp_perspective of PyCapsule object at 0x7f618fc6fa50>

which is the Python function we will later use to invoke our custom operator.

Using the TorchScript Custom Operator in Python

Once our custom operator is built into a shared library we are ready to use this operator in our TorchScript models in Python. There are two parts to this: first loading the operator into Python, and second using the operator in TorchScript code.

You already saw how to import your operator into Python: torch.ops.load_library(). This function takes the path to a shared library containing custom operators, and loads it into the current process. Loading the shared library will also execute the constructor of the global RegisterOperators object we placed into our custom operator implementation file. This will register our custom operator with the TorchScript compiler and allow us to use that operator in TorchScript code.

You can refer to your loaded operator as torch.ops.&lt;namespace&gt;.&lt;function&gt;, where &lt;namespace&gt; is the namespace part of your operator name, and &lt;function&gt; the function name of your operator. For the operator we wrote above, the namespace was my_ops and the function name warp_perspective, which means our operator is available as torch.ops.my_ops.warp_perspective. While this function can be used in scripted or traced TorchScript modules, we can also just use it in vanilla eager PyTorch and pass it regular PyTorch tensors:

>>> import torch
>>> torch.ops.load_library("libwarp_perspective.so")
>>> torch.ops.my_ops.warp_perspective(torch.randn(32, 32), torch.rand(3, 3))
tensor([[0.0000, 0.3218, 0.4611,  ..., 0.4636, 0.4636, 0.4636],
 [0.3746, 0.0978, 0.5005,  ..., 0.4636, 0.4636, 0.4636],
 [0.3245, 0.0169, 0.0000,  ..., 0.4458, 0.4458, 0.4458],
 ...,
 [0.1862, 0.1862, 0.1692,  ..., 0.0000, 0.0000, 0.0000],
 [0.1862, 0.1862, 0.1692,  ..., 0.0000, 0.0000, 0.0000],
 [0.1862, 0.1862, 0.1692,  ..., 0.0000, 0.0000, 0.0000]])

Note

What happens behind the scenes is that the first time you access torch.ops.namespace.function in Python, the TorchScript compiler (in C++ land) will see if a function namespace::function has been registered, and if so, return a Python handle to this function that we can subsequently use to call into our C++ operator implementation from Python. This is one noteworthy difference between TorchScript custom operators and C++ extensions: C++ extensions are bound manually using pybind11, while TorchScript custom ops are bound on the fly by PyTorch itself. Pybind11 gives you more flexibility with regards to what types and classes you can bind into Python and is thus recommended for purely eager code, but it is not supported for TorchScript ops.

From here on, you can use your custom operator in scripted or traced code just as you would other functions from the torch package. In fact, “standard library” functions like torch.matmul go through largely the same registration path as custom operators, which makes custom operators really first-class citizens when it comes to how and where they can be used in TorchScript.

Using the Custom Operator with Tracing

Let’s start by embedding our operator in a traced function. Recall that for tracing, we start with some vanilla Pytorch code:

def compute(x, y, z):
    return x.matmul(y) + torch.relu(z)

and then call torch.jit.trace on it. We further pass torch.jit.trace some example inputs, which it will forward to our implementation to record the sequence of operations that occur as the inputs flow through it. The result of this is effectively a “frozen” version of the eager PyTorch program, which the TorchScript compiler can further analyze, optimize and serialize:

>>> inputs = [torch.randn(4, 8), torch.randn(8, 5), torch.randn(4, 5)]
>>> trace = torch.jit.trace(compute, inputs)
>>> print(trace.graph)
graph(%x : Float(4, 8)
 %y : Float(8, 5)
 %z : Float(4, 5)) {
 %3 : Float(4, 5) = aten::matmul(%x, %y)
 %4 : Float(4, 5) = aten::relu(%z)
 %5 : int = prim::Constant[value=1]()
 %6 : Float(4, 5) = aten::add(%3, %4, %5)
 return (%6);
}

Now, the exciting revelation is that we can simply drop our custom operator into our PyTorch trace as if it were torch.relu or any other torch function:

torch.ops.load_library("libwarp_perspective.so")

def compute(x, y, z):
    x = torch.ops.my_ops.warp_perspective(x, torch.eye(3))
    return x.matmul(y) + torch.relu(z)

and then trace it as before:

>>> inputs = [torch.randn(4, 8), torch.randn(8, 5), torch.randn(8, 5)]
>>> trace = torch.jit.trace(compute, inputs)
>>> print(trace.graph)
graph(%x.1 : Float(4, 8)
 %y : Float(8, 5)
 %z : Float(8, 5)) {
 %3 : int = prim::Constant[value=3]()
 %4 : int = prim::Constant[value=6]()
 %5 : int = prim::Constant[value=0]()
 %6 : int[] = prim::Constant[value=[0, -1]]()
 %7 : Float(3, 3) = aten::eye(%3, %4, %5, %6)
 %x : Float(8, 8) = my_ops::warp_perspective(%x.1, %7)
 %11 : Float(8, 5) = aten::matmul(%x, %y)
 %12 : Float(8, 5) = aten::relu(%z)
 %13 : int = prim::Constant[value=1]()
 %14 : Float(8, 5) = aten::add(%11, %12, %13)
 return (%14);
 }

Integrating TorchScript custom ops into traced PyTorch code is as easy as this!

Using the Custom Operator with Script

Besides tracing, another way to arrive at a TorchScript representation of a PyTorch program is to directly write your code in TorchScript. TorchScript is largely a subset of the Python language, with some restrictions that make it easier for the TorchScript compiler to reason about programs. You turn your regular PyTorch code into TorchScript by annotating it with @torch.jit.script for free functions and @torch.jit.script_method for methods in a class (which must also derive from torch.jit.ScriptModule). See here for more details on TorchScript annotations.

One particular reason to use TorchScript instead of tracing is that tracing is unable to capture control flow in PyTorch code. As such, let us consider this function which does use control flow:

def compute(x, y):
  if bool(x[0][0] == 42):
      z = 5
  else:
      z = 10
  return x.matmul(y) + z

To convert this function from vanilla PyTorch to TorchScript, we annotate it with @torch.jit.script:

@torch.jit.script
def compute(x, y):
  if bool(x[0][0] == 42):
      z = 5
  else:
      z = 10
  return x.matmul(y) + z

This will just-in-time compile the compute function into a graph representation, which we can inspect in the compute.graph property:

>>> compute.graph
graph(%x : Dynamic
 %y : Dynamic) {
 %14 : int = prim::Constant[value=1]()
 %2 : int = prim::Constant[value=0]()
 %7 : int = prim::Constant[value=42]()
 %z.1 : int = prim::Constant[value=5]()
 %z.2 : int = prim::Constant[value=10]()
 %4 : Dynamic = aten::select(%x, %2, %2)
 %6 : Dynamic = aten::select(%4, %2, %2)
 %8 : Dynamic = aten::eq(%6, %7)
 %9 : bool = prim::TensorToBool(%8)
 %z : int = prim::If(%9)
 block0() {
 -> (%z.1)
 }
 block1() {
 -> (%z.2)
 }
 %13 : Dynamic = aten::matmul(%x, %y)
 %15 : Dynamic = aten::add(%13, %z, %14)
 return (%15);
}

And now, just like before, we can use our custom operator like any other function inside of our script code:

torch.ops.load_library("libwarp_perspective.so")

@torch.jit.script
def compute(x, y):
  if bool(x[0] == 42):
      z = 5
  else:
      z = 10
  x = torch.ops.my_ops.warp_perspective(x, torch.eye(3))
  return x.matmul(y) + z

When the TorchScript compiler sees the reference to torch.ops.my_ops.warp_perspective, it will find the implementation we registered via the RegisterOperators object in C++, and compile it into its graph representation:

>>> compute.graph
graph(%x.1 : Dynamic
 %y : Dynamic) {
 %20 : int = prim::Constant[value=1]()
 %16 : int[] = prim::Constant[value=[0, -1]]()
 %14 : int = prim::Constant[value=6]()
 %2 : int = prim::Constant[value=0]()
 %7 : int = prim::Constant[value=42]()
 %z.1 : int = prim::Constant[value=5]()
 %z.2 : int = prim::Constant[value=10]()
 %13 : int = prim::Constant[value=3]()
 %4 : Dynamic = aten::select(%x.1, %2, %2)
 %6 : Dynamic = aten::select(%4, %2, %2)
 %8 : Dynamic = aten::eq(%6, %7)
 %9 : bool = prim::TensorToBool(%8)
 %z : int = prim::If(%9)
 block0() {
 -> (%z.1)
 }
 block1() {
 -> (%z.2)
 }
 %17 : Dynamic = aten::eye(%13, %14, %2, %16)
 %x : Dynamic = my_ops::warp_perspective(%x.1, %17)
 %19 : Dynamic = aten::matmul(%x, %y)
 %21 : Dynamic = aten::add(%19, %z, %20)
 return (%21);
 }

Notice in particular the reference to my_ops::warp_perspective at the end of the graph.

Attention

The TorchScript graph representation is still subject to change. Do not rely on it looking like this.

And that’s really it when it comes to using our custom operator in Python. In short, you import the library containing your operator(s) using torch.ops.load_library, and call your custom op like any other torch operator from your traced or scripted TorchScript code.

Using the TorchScript Custom Operator in C++

One useful feature of TorchScript is the ability to serialize a model into an on-disk file. This file can be sent over the wire, stored in a file system or, more importantly, be dynamically deserialized and executed without needing to keep the original source code around. This is possible in Python, but also in C++. For this, PyTorch provides a pure C++ API for deserializing as well as executing TorchScript models. If you haven’t yet, please read the tutorial on loading and running serialized TorchScript models in C++, on which the next few paragraphs will build.

In short, custom operators can be executed just like regular torch operators even when deserialized from a file and run in C++. The only requirement for this is to link the custom operator shared library we built earlier with the C++ application in which we execute the model. In Python, this worked simply calling torch.ops.load_library. In C++, you need to link the shared library with your main application in whatever build system you are using. The following example will showcase this using CMake.

Note

Technically, you can also dynamically load the shared library into your C++ application at runtime in much the same way we did it in Python. On Linux, you can do this with dlopen. There exist equivalents on other platforms.

Building on the C++ execution tutorial linked above, let’s start with a minimal C++ application in one file, main.cpp in a different folder from our custom operator, that loads and executes a serialized TorchScript model:

#include <torch/script.h> // One-stop header.

#include <iostream>
#include <memory>

int main(int argc, const char* argv[]) {
  if (argc != 2) {
    std::cerr << "usage: example-app <path-to-exported-script-module>\n";
    return -1;
  }

  // Deserialize the ScriptModule from a file using torch::jit::load().
  std::shared_ptr<torch::jit::script::Module> module = torch::jit::load(argv[1]);

  std::vector<torch::jit::IValue> inputs;
  inputs.push_back(torch::randn({4, 8}));
  inputs.push_back(torch::randn({8, 5}));

  torch::Tensor output = module->forward(std::move(inputs)).toTensor();

  std::cout << output << std::endl;
}

Along with a small CMakeLists.txt file:

cmake_minimum_required(VERSION 3.1 FATAL_ERROR)
project(example_app)

find_package(Torch REQUIRED)

add_executable(example_app main.cpp)
target_link_libraries(example_app "${TORCH_LIBRARIES}")
target_compile_features(example_app PRIVATE cxx_range_for)

At this point, we should be able to build the application:

$ mkdir build
$ cd build
$ cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Looking for pthread.h
-- Looking for pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Found torch: /libtorch/lib/libtorch.so
-- Configuring done
-- Generating done
-- Build files have been written to: /example_app/build
$ make -j
Scanning dependencies of target example_app
[ 50%] Building CXX object CMakeFiles/example_app.dir/main.cpp.o
[100%] Linking CXX executable example_app
[100%] Built target example_app

And run it without passing a model just yet:

$ ./example_app
usage: example_app <path-to-exported-script-module>

Next, let’s serialize the script function we wrote earlier that uses our custom operator:

torch.ops.load_library("libwarp_perspective.so")

@torch.jit.script
def compute(x, y):
  if bool(x[0][0] == 42):
      z = 5
  else:
      z = 10
  x = torch.ops.my_ops.warp_perspective(x, torch.eye(3))
  return x.matmul(y) + z

compute.save("example.pt")

The last line will serialize the script function into a file called “example.pt”. If we then pass this serialized model to our C++ application, we can run it straight away:

$ ./example_app example.pt
terminate called after throwing an instance of 'torch::jit::script::ErrorReport'
what():
Schema not found for node. File a bug report.
Node: %16 : Dynamic = my_ops::warp_perspective(%0, %19)

Or maybe not. Maybe not just yet. Of course! We haven’t linked the custom operator library with our application yet. Let’s do this right now, and to do it properly let’s update our file organization slightly, to look like this:

example_app/
  CMakeLists.txt
  main.cpp
  warp_perspective/
    CMakeLists.txt
    op.cpp

This will allow us to add the warp_perspective library CMake target as a subdirectory of our application target. The top level CMakeLists.txt in the example_app folder should look like this:

cmake_minimum_required(VERSION 3.1 FATAL_ERROR)
project(example_app)

find_package(Torch REQUIRED)

add_subdirectory(warp_perspective)

add_executable(example_app main.cpp)
target_link_libraries(example_app "${TORCH_LIBRARIES}")
target_link_libraries(example_app -Wl,--no-as-needed warp_perspective)
target_compile_features(example_app PRIVATE cxx_range_for)

This basic CMake configuration looks much like before, except that we add the warp_perspective CMake build as a subdirectory. Once its CMake code runs, we link our example_app application with the warp_perspective shared library.

Attention

There is one crucial detail embedded in the above example: The -Wl,--no-as-needed prefix to the warp_perspective link line. This is required because we will not actually be calling any function from the warp_perspective shared library in our application code. We only need the global RegisterOperators object’s constructor to run. Inconveniently, this confuses the linker and makes it think it can just skip linking against the library altogether. On Linux, the -Wl,--no-as-needed flag forces the link to happen (NB: this flag is specific to Linux!). There are other workarounds for this. The simplest is to define some function in the operator library that you need to call from the main application. This could be as simple as a function void init(); declared in some header, which is then defined as void init() { } in the operator library. Calling this init() function in the main application will give the linker the impression that this is a library worth linking against. Unfortunately, this is outside of our control, and we would rather let you know the reason and the simple workaround for this than handing you some opaque macro to plop in your code.

Now, since we find the Torch package at the top level now, the CMakeLists.txt file in the warp_perspective subdirectory can be shortened a bit. It should look like this:

find_package(OpenCV REQUIRED)
add_library(warp_perspective SHARED op.cpp)
target_compile_features(warp_perspective PRIVATE cxx_range_for)
target_link_libraries(warp_perspective PRIVATE "${TORCH_LIBRARIES}")
target_link_libraries(warp_perspective PRIVATE opencv_core opencv_photo)

Let’s re-build our example app, which will also link with the custom operator library. In the top level example_app directory:

$ mkdir build
$ cd build
$ cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
-- The C compiler identification is GNU 5.4.0
-- The CXX compiler identification is GNU 5.4.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Looking for pthread.h
-- Looking for pthread.h - found
-- Looking for pthread_create
-- Looking for pthread_create - not found
-- Looking for pthread_create in pthreads
-- Looking for pthread_create in pthreads - not found
-- Looking for pthread_create in pthread
-- Looking for pthread_create in pthread - found
-- Found Threads: TRUE
-- Found torch: /libtorch/lib/libtorch.so
-- Configuring done
-- Generating done
-- Build files have been written to: /warp_perspective/example_app/build
$ make -j
Scanning dependencies of target warp_perspective
[ 25%] Building CXX object warp_perspective/CMakeFiles/warp_perspective.dir/op.cpp.o
[ 50%] Linking CXX shared library libwarp_perspective.so
[ 50%] Built target warp_perspective
Scanning dependencies of target example_app
[ 75%] Building CXX object CMakeFiles/example_app.dir/main.cpp.o
[100%] Linking CXX executable example_app
[100%] Built target example_app

If we now run the example_app binary and hand it our serialized model, we should arrive at a happy ending:

$ ./example_app example.pt
11.4125   5.8262   9.5345   8.6111  12.3997
 7.4683  13.5969   9.0850  11.0698   9.4008
 7.4597  15.0926  12.5727   8.9319   9.0666
 9.4834  11.1747   9.0162  10.9521   8.6269
10.0000  10.0000  10.0000  10.0000  10.0000
10.0000  10.0000  10.0000  10.0000  10.0000
10.0000  10.0000  10.0000  10.0000  10.0000
10.0000  10.0000  10.0000  10.0000  10.0000
[ Variable[CPUFloatType]{8,5} ]

Success! You are now ready to inference away.

Conclusion

This tutorial walked you throw how to implement a custom TorchScript operator in C++, how to build it into a shared library, how to use it in Python to define TorchScript models and lastly how to load it into a C++ application for inference workloads. You are now ready to extend your TorchScript models with C++ operators that interface with third party C++ libraries, write custom high performance CUDA kernels, or implement any other use case that requires the lines between Python, TorchScript and C++ to blend smoothly.

As always, if you run into any problems or have questions, you can use our forum or GitHub issues to get in touch. Also, our frequently asked questions (FAQ) page may have helpful information.

Appendix A: More Ways of Building Custom Operators

The section “Building the Custom Operator” explained how to build a custom operator into a shared library using CMake. This appendix outlines two further approaches for compilation. Both of them use Python as the “driver” or “interface” to the compilation process. Also, both re-use the existing infrastructure PyTorch provides for C++ extensions, which are the vanilla (eager) PyTorch equivalent of TorchScript custom operators that rely on pybind11 for “explicit” binding of functions from C++ into Python.

The first approach uses C++ extensions’ convenient just-in-time (JIT) compilation interface to compile your code in the background of your PyTorch script the first time you run it. The second approach relies on the venerable setuptools package and involves writing a separate setup.py file. This allows more advanced configuration as well as integration with other setuptools-based projects. We will explore both approaches in detail below.

Building with JIT compilation

The JIT compilation feature provided by the PyTorch C++ extension toolkit allows embedding the compilation of your custom operator directly into your Python code, e.g. at the top of your training script.

Note

“JIT compilation” here has nothing to do with the JIT compilation taking place in the TorchScript compiler to optimize your program. It simply means that your custom operator C++ code will be compiled in a folder under your system’s <cite>/tmp</cite> directory the first time you import it, as if you had compiled it yourself beforehand.

This JIT compilation feature comes in two flavors. In the first, you still keep your operator implementation in a separate file (op.cpp), and then use torch.utils.cpp_extension.load() to compile your extension. Usually, this function will return the Python module exposing your C++ extension. However, since we are not compiling our custom operator into its own Python module, we only want to compile a plain shared library . Fortunately, torch.utils.cpp_extension.load() has an argument is_python_module which we can set to False to indicate that we are only interested in building a shared library and not a Python module. torch.utils.cpp_extension.load() will then compile and also load the shared library into the current process, just like torch.ops.load_library did before:

import torch.utils.cpp_extension

torch.utils.cpp_extension.load(
    name="warp_perspective",
    sources=["op.cpp"],
    extra_ldflags=["-lopencv_core", "-lopencv_imgproc"],
    is_python_module=False,
    verbose=True
)

print(torch.ops.my_ops.warp_perspective)

This should approximately print:

<built-in method my_ops::warp_perspective of PyCapsule object at 0x7f3e0f840b10>

The second flavor of JIT compilation allows you to pass the source code for your custom TorchScript operator as a string. For this, use torch.utils.cpp_extension.load_inline:

import torch
import torch.utils.cpp_extension

op_source = """
#include <opencv2/opencv.hpp>
#include <torch/script.h>

torch::Tensor warp_perspective(torch::Tensor image, torch::Tensor warp) {
 cv::Mat image_mat(/*rows=*/image.size(0),
 /*cols=*/image.size(1),
 /*type=*/CV_32FC1,
 /*data=*/image.data<float>());
 cv::Mat warp_mat(/*rows=*/warp.size(0),
 /*cols=*/warp.size(1),
 /*type=*/CV_32FC1,
 /*data=*/warp.data<float>());

 cv::Mat output_mat;
 cv::warpPerspective(image_mat, output_mat, warp_mat, /*dsize=*/{64, 64});

 torch::Tensor output =
 torch::from_blob(output_mat.ptr<float>(), /*sizes=*/{64, 64});
 return output.clone();
}

static auto registry =
 torch::jit::RegisterOperators("my_ops::warp_perspective", &warp_perspective);
"""

torch.utils.cpp_extension.load_inline(
    name="warp_perspective",
    cpp_sources=op_source,
    extra_ldflags=["-lopencv_core", "-lopencv_imgproc"],
    is_python_module=False,
    verbose=True,
)

print(torch.ops.my_ops.warp_perspective)

Naturally, it is best practice to only use torch.utils.cpp_extension.load_inline if your source code is reasonably short.

Building with Setuptools

The second approach to building our custom operator exclusively from Python is to use setuptools. This has the advantage that setuptools has a quite powerful and extensive interface for building Python modules written in C++. However, since setuptools is really intended for building Python modules and not plain shared libraries (which do not have the necessary entry points Python expects from a module), this route can be slightly quirky. That said, all you need is a setup.py file in place of the CMakeLists.txt which looks like this:

Notice that we enabled the no_python_abi_suffix option in the BuildExtension at the bottom. This instructs setuptools to omit any Python-3 specific ABI suffixes in the name of the produced shared library. Otherwise, on Python 3.7 for example, the library may be called warp_perspective.cpython-37m-x86_64-linux-gnu.so where cpython-37m-x86_64-linux-gnu is the ABI tag, but we really just want it to be called warp_perspective.so

If we now run python setup.py build develop in a terminal from within the folder in which setup.py is situated, we should see something like:

$ python setup.py build develop
running build
running build_ext
building 'warp_perspective' extension
creating build
creating build/temp.linux-x86_64-3.7
gcc -pthread -B /root/local/miniconda/compiler_compat -Wl,--sysroot=/ -Wsign-compare -DNDEBUG -g -fwrapv -O3 -Wall -Wstrict-prototypes -fPIC -I/root/local/miniconda/lib/python3.7/site-packages/torch/lib/include -I/root/local/miniconda/lib/python3.7/site-packages/torch/lib/include/torch/csrc/api/include -I/root/local/miniconda/lib/python3.7/site-packages/torch/lib/include/TH -I/root/local/miniconda/lib/python3.7/site-packages/torch/lib/include/THC -I/root/local/miniconda/include/python3.7m -c op.cpp -o build/temp.linux-x86_64-3.7/op.o -DTORCH_API_INCLUDE_EXTENSION_H -DTORCH_EXTENSION_NAME=warp_perspective -D_GLIBCXX_USE_CXX11_ABI=0 -std=c++11
cc1plus: warning: command line option-Wstrict-prototypesis valid for C/ObjC but not for C++
creating build/lib.linux-x86_64-3.7
g++ -pthread -shared -B /root/local/miniconda/compiler_compat -L/root/local/miniconda/lib -Wl,-rpath=/root/local/miniconda/lib -Wl,--no-as-needed -Wl,--sysroot=/ build/temp.linux-x86_64-3.7/op.o -lopencv_core -lopencv_imgproc -o build/lib.linux-x86_64-3.7/warp_perspective.so
running develop
running egg_info
creating warp_perspective.egg-info
writing warp_perspective.egg-info/PKG-INFO
writing dependency_links to warp_perspective.egg-info/dependency_links.txt
writing top-level names to warp_perspective.egg-info/top_level.txt
writing manifest file 'warp_perspective.egg-info/SOURCES.txt'
reading manifest file 'warp_perspective.egg-info/SOURCES.txt'
writing manifest file 'warp_perspective.egg-info/SOURCES.txt'
running build_ext
copying build/lib.linux-x86_64-3.7/warp_perspective.so ->
Creating /root/local/miniconda/lib/python3.7/site-packages/warp-perspective.egg-link (link to .)
Adding warp-perspective 0.0.0 to easy-install.pth file

Installed /warp_perspective
Processing dependencies for warp-perspective==0.0.0
Finished processing dependencies for warp-perspective==0.0.0

This will produce a shared library called warp_perspective.so, which we can pass to torch.ops.load_library as we did earlier to make our operator visible to TorchScript: