Shortcuts

在C++中加载TorchScript模型

创建于:2018年9月14日 | 最后更新:2024年12月2日 | 最后验证:2024年11月5日

警告

TorchScript 不再处于积极开发阶段。

正如其名所示,PyTorch的主要接口是Python编程语言。虽然Python是许多需要动态性和迭代简便性的场景中合适且首选的语言,但同样存在许多情况下Python的这些特性并不理想。一个经常适用后者的环境是生产——低延迟和严格部署要求的领域。对于生产场景,C++通常是首选语言,即使只是为了将其绑定到另一种语言如Java、Rust或Go中。以下段落将概述PyTorch提供的从现有Python模型到可以纯粹从C++加载执行的序列化表示的路径,且不依赖于Python。

第一步:将您的PyTorch模型转换为Torch脚本

PyTorch模型从Python到C++的旅程是通过Torch Script实现的,Torch Script是一种可以被Torch Script编译器理解、编译和序列化的PyTorch模型表示。如果你从一个用原生“eager”API编写的现有PyTorch模型开始,你必须首先将你的模型转换为Torch Script。在下面讨论的最常见情况下,这只需要很少的努力。如果你已经有一个Torch Script模块,你可以跳过本教程的下一部分。

存在两种将PyTorch模型转换为Torch Script的方法。第一种称为追踪,这是一种通过使用示例输入评估模型一次并记录这些输入在模型中的流动来捕获模型结构的机制。这种方法适用于有限使用控制流的模型。第二种方法是在模型中添加显式注释,告知Torch Script编译器它可以直接解析和编译模型代码,但需遵守Torch Script语言的约束。

提示

您可以在官方的Torch Script参考中找到这两种方法的完整文档,以及关于使用哪种方法的进一步指导。

通过追踪转换为Torch脚本

要通过跟踪将PyTorch模型转换为Torch Script,您必须将模型实例以及示例输入传递给torch.jit.trace函数。这将生成一个torch.jit.ScriptModule对象,其中嵌入了模型评估的跟踪信息在模块的forward方法中:

import torch
import torchvision

# An instance of your model.
model = torchvision.models.resnet18()

# An example input you would normally provide to your model's forward() method.
example = torch.rand(1, 3, 224, 224)

# Use torch.jit.trace to generate a torch.jit.ScriptModule via tracing.
traced_script_module = torch.jit.trace(model, example)

追踪的 ScriptModule 现在可以像常规的 PyTorch 模块一样进行评估:

In[1]: output = traced_script_module(torch.ones(1, 3, 224, 224))
In[2]: output[0, :5]
Out[2]: tensor([-0.2698, -0.0381,  0.4023, -0.3010, -0.0448], grad_fn=<SliceBackward>)

通过注释转换为 Torch 脚本

在某些情况下,例如如果您的模型采用了特定形式的控制流,您可能希望直接在Torch Script中编写模型并相应地注释您的模型。例如,假设您有以下普通的Pytorch模型:

import torch

class MyModule(torch.nn.Module):
    def __init__(self, N, M):
        super(MyModule, self).__init__()
        self.weight = torch.nn.Parameter(torch.rand(N, M))

    def forward(self, input):
        if input.sum() > 0:
          output = self.weight.mv(input)
        else:
          output = self.weight + input
        return output

由于此模块的forward方法使用了依赖于输入的控制流,因此不适合进行跟踪。相反,我们可以将其转换为ScriptModule。为了将模块转换为ScriptModule,需要使用torch.jit.script编译模块,如下所示:

class MyModule(torch.nn.Module):
    def __init__(self, N, M):
        super(MyModule, self).__init__()
        self.weight = torch.nn.Parameter(torch.rand(N, M))

    def forward(self, input):
        if input.sum() > 0:
          output = self.weight.mv(input)
        else:
          output = self.weight + input
        return output

my_module = MyModule(10,20)
sm = torch.jit.script(my_module)

如果你需要在你的nn.Module中排除一些方法, 因为它们使用了TorchScript目前还不支持的Python特性, 你可以用@torch.jit.ignore来注释这些方法。

sm 是一个准备好进行序列化的 ScriptModule 实例。

步骤2:将您的脚本模块序列化到文件

一旦你有了一个ScriptModule,无论是通过追踪还是注释一个PyTorch模型,你都准备好将其序列化到一个文件中。之后,你将能够在C++中从这个文件加载模块并执行它,而不需要任何Python依赖。假设我们想要序列化之前在追踪示例中展示的ResNet18模型。要执行这个序列化,只需在模块上调用save并传递一个文件名:

traced_script_module.save("traced_resnet_model.pt")

这将在您的工作目录中生成一个traced_resnet_model.pt文件。 如果您还想序列化sm,请调用sm.save("my_module_model.pt")。 我们现在正式离开了Python的领域,准备进入C++的领域。

步骤3:在C++中加载您的脚本模块

要在C++中加载你的序列化PyTorch模型,你的应用程序必须依赖于PyTorch C++ API——也称为LibTorch。LibTorch发行版包含一组共享库、头文件和CMake构建配置文件。虽然CMake不是依赖LibTorch的必要条件,但它是推荐的方法,并且将来会得到很好的支持。在本教程中,我们将使用CMake和LibTorch构建一个最小的C++应用程序,该应用程序只需加载并执行一个序列化的PyTorch模型。

一个最小的C++应用程序

让我们从讨论加载模块的代码开始。以下代码已经可以实现:

#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;
  }


  torch::jit::script::Module module;
  try {
    // Deserialize the ScriptModule from a file using torch::jit::load().
    module = torch::jit::load(argv[1]);
  }
  catch (const c10::Error& e) {
    std::cerr << "error loading the model\n";
    return -1;
  }

  std::cout << "ok\n";
}

头文件包含了运行示例所需的 LibTorch 库中的所有相关包含。我们的应用程序接受一个序列化的 PyTorch ScriptModule 的文件路径作为其唯一的命令行参数,然后使用 torch::jit::load() 函数对该模块进行反序列化,该函数将此文件路径作为输入。作为返回,我们得到一个 torch::jit::script::Module 对象。我们稍后将研究如何执行它。

依赖LibTorch并构建应用程序

假设我们将上述代码存储到一个名为example-app.cpp的文件中。一个最小的CMakeLists.txt来构建它可能看起来像这样:

cmake_minimum_required(VERSION 3.0 FATAL_ERROR)
project(custom_ops)

find_package(Torch REQUIRED)

add_executable(example-app example-app.cpp)
target_link_libraries(example-app "${TORCH_LIBRARIES}")
set_property(TARGET example-app PROPERTY CXX_STANDARD 17)

构建示例应用程序所需的最后一件事是LibTorch发行版。您始终可以从PyTorch网站上的下载页面获取最新的稳定版本。如果您下载并解压缩最新的存档,您应该会收到一个具有以下目录结构的文件夹:

libtorch/
  bin/
  include/
  lib/
  share/
  • lib/ 文件夹包含您必须链接的共享库,

  • include/ 文件夹包含您的程序需要包含的头文件,

  • share/ 文件夹包含必要的 CMake 配置,以启用上面简单的 find_package(Torch) 命令。

提示

在Windows上,调试版和发布版的构建不兼容ABI。如果您计划在调试模式下构建您的项目,请尝试使用LibTorch的调试版本。同时,请确保在下面的cmake --build .行中指定正确的配置。

最后一步是构建应用程序。为此,假设我们的示例目录布局如下:

example-app/
  CMakeLists.txt
  example-app.cpp

我们现在可以从example-app/文件夹中运行以下命令来构建应用程序:

mkdir build
cd build
cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
cmake --build . --config Release

其中 /path/to/libtorch 应该是解压后的 LibTorch 发行版的完整路径。如果一切顺利,它看起来会像这样:

root@4b5a67132e81:/example-app# mkdir build
root@4b5a67132e81:/example-app# cd build
root@4b5a67132e81:/example-app/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
-- Configuring done
-- Generating done
-- Build files have been written to: /example-app/build
root@4b5a67132e81:/example-app/build# make
Scanning dependencies of target example-app
[ 50%] Building CXX object CMakeFiles/example-app.dir/example-app.cpp.o
[100%] Linking CXX executable example-app
[100%] Built target example-app

如果我们提供之前创建的跟踪ResNet18模型traced_resnet_model.pt的路径 到生成的example-app二进制文件,我们应该会得到一个友好的 “ok”。请注意,如果尝试使用my_module_model.pt运行此示例,您将收到一个错误消息,指出 您的输入形状不兼容。my_module_model.pt期望的是1D而不是4D。

root@4b5a67132e81:/example-app/build# ./example-app <path_to_model>/traced_resnet_model.pt
ok

步骤4:在C++中执行脚本模块

成功加载了我们在C++中序列化的ResNet18后,我们现在只需几行代码就可以执行它了!让我们将这些行添加到我们的C++应用程序的main()函数中:

// Create a vector of inputs.
std::vector<torch::jit::IValue> inputs;
inputs.push_back(torch::ones({1, 3, 224, 224}));

// Execute the model and turn its output into a tensor.
at::Tensor output = module.forward(inputs).toTensor();
std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';

前两行设置了模型的输入。我们创建了一个torch::jit::IValue的向量(script::Module方法接受并返回的类型擦除值类型),并添加了一个输入。为了创建输入张量,我们使用了torch::ones(),相当于C++ API中的torch.ones。然后我们运行script::Moduleforward方法,将我们创建的输入向量传递给它。作为返回,我们得到了一个新的IValue,我们通过调用toTensor()将其转换为张量。

提示

要了解更多关于像torch::ones这样的函数以及PyTorch C++ API的信息,请参考其文档https://pytorch.org/cppdocs。PyTorch C++ API提供了与Python API几乎相同的功能,允许你像在Python中一样进一步操作和处理张量。

在最后一行,我们打印输出的前五个条目。由于我们在本教程的早期在Python中向我们的模型提供了相同的输入,理想情况下我们应该看到相同的输出。让我们通过重新编译我们的应用程序并使用相同的序列化模型运行它来尝试一下:

root@4b5a67132e81:/example-app/build# make
Scanning dependencies of target example-app
[ 50%] Building CXX object CMakeFiles/example-app.dir/example-app.cpp.o
[100%] Linking CXX executable example-app
[100%] Built target example-app
root@4b5a67132e81:/example-app/build# ./example-app traced_resnet_model.pt
-0.2698 -0.0381  0.4023 -0.3010 -0.0448
[ Variable[CPUFloatType]{1,5} ]

作为参考,之前在Python中的输出是:

tensor([-0.2698, -0.0381,  0.4023, -0.3010, -0.0448], grad_fn=<SliceBackward>)

看起来是个不错的匹配!

提示

要将您的模型移动到GPU内存中,您可以编写model.to(at::kCUDA);。 确保模型的输入也位于CUDA内存中, 通过调用tensor.to(at::kCUDA),它将在CUDA内存中返回一个新的张量。

第5步:获取帮助并探索API

本教程希望为您提供了从Python到C++的PyTorch模型路径的总体理解。通过本教程中描述的概念,您应该能够从一个普通的、“急切”的PyTorch模型,到Python中的编译ScriptModule,再到磁盘上的序列化文件,最后——完成循环——到C++中的可执行script::Module

当然,我们并没有涵盖所有的概念。例如,您可能会发现自己想要用C++或CUDA实现的自定义操作符来扩展您的ScriptModule,并在您的纯C++生产环境中加载的ScriptModule中执行这个自定义操作符。好消息是:这是可能的,并且得到了很好的支持!现在,您可以探索这个文件夹中的示例,我们很快就会跟进一个教程。在此期间,以下链接可能会对您有所帮助:

一如既往,如果您遇到任何问题或有疑问,您可以使用我们的 论坛GitHub 问题 来联系我们。