Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/main/advanced_source/torch_script_custom_ops.rst
Views: 712
Extending TorchScript with Custom C++ Operators =============================================== .. warning:: This tutorial is deprecated as of PyTorch 2.4. Please see :ref:`custom-ops-landing-page` for the newest up-to-date guides on PyTorch Custom Operators. The PyTorch 1.0 release introduced a new programming model to PyTorch called `TorchScript <https://pytorch.org/docs/master/jit.html>`_. 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 <https://pytorch.org/cppdocs/#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 <https://www.opencv.org>`_, 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 ``Mat``), how to register your operator with the TorchScript runtime and finally how to compile the operator and use it in Python and C++. Implementing the Custom Operator in C++ --------------------------------------- For this tutorial, we'll be exposing the `warpPerspective <https://docs.opencv.org/2.4/modules/imgproc/doc/geometric_transformations.html#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: .. literalinclude:: ../advanced_source/torch_script_custom_ops/op.cpp :language: cpp :start-after: BEGIN warp_perspective :end-before: END warp_perspective 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 <https://pytorch.org/cppdocs/notes/tensor_basics.html>`_ for more information about ATen, the library that provides the ``Tensor`` class to PyTorch. Further, `this tutorial <https://pytorch.org/cppdocs/notes/tensor_creation.html>`_ 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, .. literalinclude:: ../advanced_source/torch_script_custom_ops/op.cpp :language: cpp :start-after: BEGIN image_mat :end-before: END image_mat we are calling `this constructor <https://docs.opencv.org/trunk/d3/d63/classcv_1_1Mat.html#a922de793eabcec705b3579c5f95a643e>`_ 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: .. literalinclude:: ../advanced_source/torch_script_custom_ops/op.cpp :language: cpp :start-after: BEGIN warp_mat :end-before: END warp_mat 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: .. literalinclude:: ../advanced_source/torch_script_custom_ops/op.cpp :language: cpp :start-after: BEGIN output_mat :end-before: END output_mat 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: .. literalinclude:: ../advanced_source/torch_script_custom_ops/op.cpp :language: cpp :start-after: BEGIN output_tensor :end-before: END output_tensor We use the ``.ptr<float>()`` method on the OpenCV ``Mat`` class to get a raw pointer to the underlying data (just like ``.data_ptr<float>()`` 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. If you have ever used the pybind11 library, our syntax for registration resembles the pybind11 syntax very closely. To register a single function, we write: .. literalinclude:: ../advanced_source/torch_script_custom_ops/op.cpp :language: cpp :start-after: BEGIN registry :end-before: END registry somewhere at the top level of our ``op.cpp`` file. The ``TORCH_LIBRARY`` macro creates a function that will be called when your program starts. The name of your library (``my_ops``) is given as the first argument (it should not be in quotes). The second argument (``m``) defines a variable of type ``torch::Library`` which is the main interface to register your operators. The method ``Library::def`` actually creates an operator named ``warp_perspective``, exposing it to both Python and TorchScript. You can define as many operators as you like by making multiple calls to ``def``. Behinds the scenes, the ``def`` function is actually doing quite a bit of work: it is using template metaprogramming to inspect the type signature of your function and translate it into an operator schema which specifies the operators type within TorchScript's type system. 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 other alternatives. Environment setup ***************** We need an installation of PyTorch and OpenCV. The easiest and most platform independent way to get both is to via Conda:: conda install -c pytorch pytorch conda install opencv Building with CMake ******************* To build our custom operator into a shared library using the `CMake <https://cmake.org>`_ 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 The contents of our ``CMakeLists.txt`` file should then be the following: .. literalinclude:: ../advanced_source/torch_script_custom_ops/CMakeLists.txt :language: cpp To now build our operator, we can run the following commands from our ``warp_perspective`` folder: .. code-block:: shell $ mkdir build $ cd build $ cmake -DCMAKE_PREFIX_PATH="$(python -c 'import torch.utils; print(torch.utils.cmake_prefix_path)')" .. -- 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, we use the helper variable ``torch.utils.cmake_prefix_path`` to conveniently tell us where the cmake files for our PyTorch install are. 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: .. literalinclude:: ../advanced_source/torch_script_custom_ops/smoke_test.py :language: python 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 ``TORCH_LIBRARY`` block. 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.<namespace>.<function>``, where ``<namespace>`` is the namespace part of your operator name, and ``<function>`` 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: .. literalinclude:: ../advanced_source/torch_script_custom_ops/test.py :language: python :prepend: import torch :start-after: BEGIN preamble :end-before: END preamble producing: .. code-block:: python 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. (One difference, however, is that standard library functions have custom written Python argument parsing logic that differs from ``torch.ops`` argument parsing.) 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: .. literalinclude:: ../advanced_source/torch_script_custom_ops/test.py :language: python :start-after: BEGIN compute :end-before: END compute 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: .. literalinclude:: ../advanced_source/torch_script_custom_ops/test.py :language: python :start-after: BEGIN trace :end-before: END trace Producing:: graph(%x : Float(4:8, 8:1), %y : Float(8:5, 5:1), %z : Float(4:5, 5:1)): %3 : Float(4:5, 5:1) = aten::matmul(%x, %y) # test.py:10:0 %4 : Float(4:5, 5:1) = aten::relu(%z) # test.py:10:0 %5 : int = prim::Constant[value=1]() # test.py:10:0 %6 : Float(4:5, 5:1) = aten::add(%3, %4, %5) # test.py:10:0 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: .. literalinclude:: ../advanced_source/torch_script_custom_ops/test.py :language: python :start-after: BEGIN compute2 :end-before: END compute2 and then trace it as before: .. literalinclude:: ../advanced_source/torch_script_custom_ops/test.py :language: python :start-after: BEGIN trace2 :end-before: END trace2 Producing:: graph(%x.1 : Float(4:8, 8:1), %y : Float(8:5, 5:1), %z : Float(8:5, 5:1)): %3 : int = prim::Constant[value=3]() # test.py:25:0 %4 : int = prim::Constant[value=6]() # test.py:25:0 %5 : int = prim::Constant[value=0]() # test.py:25:0 %6 : Device = prim::Constant[value="cpu"]() # test.py:25:0 %7 : bool = prim::Constant[value=0]() # test.py:25:0 %8 : Float(3:3, 3:1) = aten::eye(%3, %4, %5, %6, %7) # test.py:25:0 %x : Float(8:8, 8:1) = my_ops::warp_perspective(%x.1, %8) # test.py:25:0 %10 : Float(8:5, 5:1) = aten::matmul(%x, %y) # test.py:26:0 %11 : Float(8:5, 5:1) = aten::relu(%z) # test.py:26:0 %12 : int = prim::Constant[value=1]() # test.py:26:0 %13 : Float(8:5, 5:1) = aten::add(%10, %11, %12) # test.py:26:0 return (%13) 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 <https://pytorch.org/docs/master/jit.html>`_ 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: .. code-block:: python 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``: .. code-block:: python @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: .. code-block:: python >>> 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: .. code-block:: python 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 ``TORCH_LIBRARY`` function in C++, and compile it into its graph representation: .. code-block:: python >>> 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 <https://pytorch.org/cppdocs/>`_ 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++ <https://pytorch.org/tutorials/advanced/cpp_export.html>`_, 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 <https://tldp.org/HOWTO/Program-Library-HOWTO/dl-libraries.html>`_. 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: .. code-block:: cpp #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(). 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: .. code-block:: cmake 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: .. code-block:: shell $ mkdir build $ cd build $ cmake -DCMAKE_PREFIX_PATH="$(python -c 'import torch.utils; print(torch.utils.cmake_prefix_path)')" .. -- 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: .. code-block:: shell $ ./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: .. code-block:: python 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: .. code-block:: shell $ ./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: .. code-block:: cmake 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 ``TORCH_LIBRARY`` function 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: .. code-block:: cmake 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: .. code-block:: shell $ mkdir build $ cd build $ cmake -DCMAKE_PREFIX_PATH="$(python -c 'import torch.utils; print(torch.utils.cmake_prefix_path)')" .. -- 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: .. code-block:: shell $ ./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 <https://discuss.pytorch.org/>`_ or `GitHub issues <https://github.com/pytorch/pytorch/issues>`_ to get in touch. Also, our `frequently asked questions (FAQ) page <https://pytorch.org/cppdocs/notes/faq.html>`_ 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 <https://pytorch.org/docs/stable/cpp_extension.html>`_ PyTorch provides for `*C++ extensions* <https://pytorch.org/tutorials/advanced/cpp_extension.html>`_, which are the vanilla (eager) PyTorch equivalent of TorchScript custom operators that rely on `pybind11 <https://github.com/pybind/pybind11>`_ for "explicit" binding of functions from C++ into Python. The first approach uses C++ extensions' `convenient just-in-time (JIT) compilation interface <https://pytorch.org/docs/stable/cpp_extension.html#torch.utils.cpp_extension.load>`_ 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 `/tmp` 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: .. code-block:: python 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: .. code-block:: python <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``: .. code-block:: python 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(); } TORCH_LIBRARY(my_ops, m) { m.def("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. Note that if you're using this in a Jupyter Notebook, you should not execute the cell with the registration multiple times because each execution registers a new library and re-registers the custom operator. If you need to re-execute it, please restart the Python kernel of your notebook beforehand. 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: .. code-block:: python from setuptools import setup from torch.utils.cpp_extension import BuildExtension, CppExtension setup( name="warp_perspective", ext_modules=[ CppExtension( "warp_perspective", ["example_app/warp_perspective/op.cpp"], libraries=["opencv_core", "opencv_imgproc"], ) ], cmdclass={"build_ext": BuildExtension.with_options(no_python_abi_suffix=True)}, ) 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: .. code-block:: shell $ 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-prototypes’ is 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: .. code-block:: python >>> import torch >>> torch.ops.load_library("warp_perspective.so") >>> print(torch.ops.my_ops.warp_perspective) <built-in method custom::warp_perspective of PyCapsule object at 0x7ff51c5b7bd0>