在 C++ 中加载 TorchScript 模型¶
顾名思义,PyTorch 的主要接口是 Python 编程语言。虽然 Python 对于许多需要动态性和易于迭代的场景来说是一种合适且首选的语言,但也有很多情况,Python 的这些特性并不理想。在生产环境中,后者通常适用——生产环境需要低延迟和严格的部署要求。对于生产场景,C++ 非常常用,即使只是将其绑定到另一种语言(如 Java、Rust 或 Go)中。以下段落将概述 PyTorch 提供的路径,从现有的 Python 模型到可序列化的表示,该表示可以完全从 C++ 中加载和执行,而无需依赖 Python。
步骤 1:将您的 PyTorch 模型转换为 Torch Script¶
PyTorch 模型从 Python 到 C++ 的转换通过 Torch Script 实现,Torch Script 是 PyTorch 模型的一种表示形式,可以被 Torch Script 编译器理解、编译和序列化。如果您是从使用普通“急切”API 编写的现有 PyTorch 模型开始,则必须首先将模型转换为 Torch Script。在下面讨论的最常见情况下,这只需要很少的努力。如果您已经拥有 Torch Script 模块,则可以跳到本教程的下一部分。
将 PyTorch 模型转换为 Torch Script 有两种方法。第一种称为 *追踪* (tracing),这种机制通过使用示例输入评估模型一次并记录这些输入在模型中的流动来捕获模型的结构。这适用于控制流使用有限的模型。第二种方法是在模型中添加显式注释,以告知 Torch Script 编译器可以直接解析和编译模型代码,但需遵守 Torch Script 语言施加的约束。
提示
您可以在官方的 Torch Script 参考文档 中找到这两种方法的完整文档,以及关于使用哪种方法的更多指导。
通过追踪转换为 Torch Script¶
要通过追踪将 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)
现在可以像评估常规 PyTorch 模块一样评估追踪后的 ScriptModule
。
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 Script¶
在某些情况下,例如如果您的模型使用了特定形式的控制流,您可能希望直接用 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:将 Script 模块序列化到文件¶
一旦获得了 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++ 中加载 Script 模块¶
要在 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";
}
<torch/script.h>
头文件包含了 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/
文件夹包含启用上面简单的find_package(Torch)
命令所需的 CMake 配置。
提示
在 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++ 中执行 Script 模块¶
在 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::Module
的 forward
方法,并将我们创建的输入向量传递给它。作为返回值,我们得到一个新的 IValue
,我们通过调用 toTensor()
将其转换为张量。
提示
要了解有关 torch::ones
等函数以及 PyTorch C++ API 的更多信息,请参阅其文档 https://pytorch.ac.cn/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¶
本教程希望为您提供对 PyTorch 模型从 Python 到 C++ 路径的一般理解。使用本教程中描述的概念,您应该能够从普通的“急切”PyTorch 模型,到 Python 中编译的 ScriptModule
,到磁盘上的序列化文件,以及最后到 C++ 中的可执行 script::Module
。
当然,还有很多概念我们没有涉及。例如,您可能会希望使用 C++ 或 CUDA 中实现的自定义运算符扩展您的 ScriptModule
,并在您纯 C++ 生产环境中加载的 ScriptModule
内部执行此自定义运算符。好消息是:这是可能的,并且得到了良好的支持!目前,您可以探索 此 文件夹以获取示例,我们很快就会推出后续教程。在此期间,以下链接可能会有帮助:
Torch Script 参考文档:https://pytorch.ac.cn/docs/master/jit.html
PyTorch C++ API 文档:https://pytorch.ac.cn/cppdocs/
PyTorch Python API 文档:https://pytorch.ac.cn/docs/