背景
- PyTorch的主要接口是Python語言。雖然Python是許多需要動態和易於迭代的場景的首選語言,但同樣有很多情況下,Python的這些屬性恰好是不利的。在生產環境中,需要保證低延遲和其它嚴格的要求,而對於生產場景,C++通常是首選語言,通常C++會被綁定到Java或Go當中;
- 第一種方法是在PyTorch中直接使用C++編寫PyTorch的前端,而不是通常情況下使用Python來編寫PyTorch的前端,實現模型定義、訓練和評估以及模型的部署;
- 第二種方法是使用Python編寫PyTorch的前端,並且實現上述功能;
- 衆所周知,Python相對於C++在不考慮執行效率的情況下具有很多優勢,本文不會討論這方面的問題。因此,如果可以使用Python編寫前端,實現模型定義、訓練和評估,而將模型的部署交由C++實現,則可以最大化目標,最快地獲得模型以及部署高效的模型。
- 概念轉換成具體的方案,將Python在PyTorch下訓練得到的模型文件轉化成C++可以加載和執行的模型文件,並且自此以後不再依賴於Python;
- PyTorch模型從Python到C ++之旅由Torch Script實現,Torch Script是PyTorch模型的一種表示,並且可以由Torch Script的編譯器理解、編譯和序列化。
環境
- 安裝PyTorch的版本爲1.0及以上;
- 安裝C++版本的LibTorch,LibTorch發行版包含一組共享庫,頭文件和CMake構建配置文件;
- 安裝Intel所提供的MKL-DNN庫,Caffe依賴此庫,編譯得到的可執行程序會依賴其中的
libmklml
和libiomp5
動態鏈接庫,若無此庫在執行程序時會有錯誤產生,安裝後將這兩個動態鏈接庫拷貝至LibTorch的lib目錄下;
Mac OS 報錯信息如下:
dyld: Library not loaded: @rpath/libmklml.dylib> dyld: Library not loaded: @rpath/libmklml.dylib
Referenced from: ******
Reason: image not found
- 安裝CMake。
將PyTorch模型轉化爲Torch Script的兩種方法
- 如果需要C++使用PyTorch的模型,就必須先將PyTorch模型轉化爲Torch Script;
- 目前有兩種方法,可以將PyTorch模型轉化爲Torch Script:
- 第一種方法是tracing。該方法通過將樣本輸入到模型中,並對該過程進行推斷從而捕獲模型的結構,並記錄該樣本在模型中的控制流。該方法適用於模型中較少使用控制流的模型;
- 第二種方法是向模型中添加顯式的註釋,使得Torch Script編譯器可以直接解析和編譯模型的代碼,受Torch Script強加的約束。該方法適用於使用特定控制流的模型,。
利用Tracing將模型轉換爲Torch Script
通過tracing的方法將PyTorch的模型轉換爲Torch Script,則必須將模型的實例以及樣本輸入傳遞給torch.jit.trace
方法。這樣會生成一個torch.jit.ScriptModule
對象,模型中的forward
方法中用預先嵌入了模型推斷的跟蹤機制:
import torch
import torchvision
# An instance of your model.
model = torchvision.models.resnet18()
# model.eval()
# 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
output = traced_script_module(torch.ones(1, 3, 224, 224))
利用註釋將模型轉換爲Torch Script
- 在某些情況下,如模型採用特定形式的控制流(
if...else...
),可以使用註釋的方法將模型轉化爲Torch Script; - 此模塊的
forward
方法使用依賴於輸入的控制流,因此它不適合利用tracing的方法生成Torch Script。可以通過繼承torch.jit.ScriptModule
並將@torch.jit.script_method
標註添加到模型的forward中的方法,來將模型轉換爲ScriptModule
。
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
import torch
class MyModule(torch.jit.ScriptModule):
def __init__(self, N, M):
super(MyModule, self).__init__()
self.weight = torch.nn.Parameter(torch.rand(N, M))
@torch.jit.script_method
def forward(self, input):
if input.sum() > 0:
output = self.weight.mv(input)
else:
output = self.weight + input
return output
my_script_module = MyModule()
- MyModule現在直接創建一個新對象會生成一個可以進行序列化的
ScriptModule
實例 。
模型序列化
不論使用了上述的哪一種方法,當ScriptModule
掌握了模型的Tracing或註釋,就可以將其序列化爲文件。稍後將能夠使用C++從該文件加載模型並執行它,不需要依賴於Python。而要執行序列化,只需在模型的實例上調用save
方法。
traced_script_module.save("model.pt")
現在正式離開Python的領域,並準備跨越到C ++領域。
使用C++加載腳本模塊
- 在C++中加載序列化的PyTorch模型,應用程序必須依賴於PyTorch C++ API,也稱爲LibTorch。LibTorch的發行版包含一組共享庫,頭文件和CMake構建配置文件。CMake是LibTorch推薦的方法,並且將來會得到很好的支持;
// example-app.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().
std::shared_ptr<torch::jit::script::Module> module = torch::jit::load(argv[1]);
assert(module != nullptr);
std::cout << "ok\n";
}
<torch/script.h>
是編譯運行上述代碼所需的LibTorch頭文件;- 接受序列化的
ScriptModule
文件作爲唯一的命令行參數; - 使用
torch::jit::load
方法反序列化文件,該方法的參數爲序列化ScriptModule
的文件; - 反序列化文件後返回共享所有權的智能指針,其類型爲
torch::jit::script::Module
,與Python中的torch.jit.ScriptModule
相對應。
使用CMake構建應用程序
// 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 11)
構建應用程序
# 目錄佈局
example-app/
CMakeLists.txt
example-app.cpp
libtorch/
bin/
include/
lib/
share/
mkdir build
cd build
cmake -DCMAKE_PREFIX_PATH=/path/to/libtorch ..
make
在main()函數添加C++代碼執行Script Module
// 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.
auto output = module->forward(inputs).toTensor();
std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/5) << '\n';
需要再次編譯,創建新的可執行文件。
make
利用註釋將模型轉換爲Torch Script的例子
# Convolutional neural network (two convolutional layers)
class ConvNet(torch.jit.ScriptModule):
def __init__(self, num_classes=10):
super(ConvNet, self).__init__()
self.layer1 = torch.jit.trace(nn.Sequential(
nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),
nn.BatchNorm2d(16),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)), torch.rand(1, 1, 28, 28))
self.layer2 = torch.jit.trace(nn.Sequential(
nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)), torch.rand(1, 16, 14, 14))
self.fc = nn.Linear(7*7*32, num_classes)
@torch.jit.script_method
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
return out
- 父類不再是
torch.nn.module
,而是torch.jit.ScriptModule
; - 使用
torch.jit.trace
跟蹤函數,跟蹤只能正確記錄不依賴於數據的函數和模塊,並且沒有任何未跟蹤的外部依賴(例如,執行輸入/輸出或訪問全局變量); - Python函數或者模塊將使用輸入數據執行,其參數和返回值必須是Tensor或者包含Tensor的元組,函數或者模塊作爲方法的第一個參數;
- 在跟蹤時將輸入數據的形狀傳遞給函數作爲方法的第二個參數;
def f(x):
return x * 2
traced_f = torch.jit.trace(f, torch.rand(1))
- 完整的轉換爲Torch Script的Python代碼(模型訓練):
from __future__ import print_function
import torch
import torch.nn as nn
# Convolutional neural network (two convolutional layers)
class ConvNet(torch.jit.ScriptModule):
def __init__(self, num_classes=10):
super(ConvNet, self).__init__()
self.layer1 = torch.jit.trace(nn.Sequential(
nn.Conv2d(1, 16, kernel_size=5, stride=1, padding=2),
nn.BatchNorm2d(16),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)), torch.rand(1, 1, 28, 28))
self.layer2 = torch.jit.trace(nn.Sequential(
nn.Conv2d(16, 32, kernel_size=5, stride=1, padding=2),
nn.BatchNorm2d(32),
nn.ReLU(),
nn.MaxPool2d(kernel_size=2, stride=2)), torch.rand(1, 16, 14, 14))
self.fc = nn.Linear(7*7*32, num_classes)
@torch.jit.script_method
def forward(self, x):
out = self.layer1(x)
out = self.layer2(out)
out = out.reshape(out.size(0), -1)
out = self.fc(out)
return out
traced_script_module = ConvNet()
traced_script_module.load_state_dict(torch.load('model.ckpt'))
traced_script_module.eval()
traced_script_module.save('model.pt')
with torch.no_grad():
outputs = traced_script_module(torch.rand(1, 1, 28, 28))
print(torch.nn.functional.softmax(outputs, dim=1))
- 完整的反序列化和執行Script Module的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;
}
// Deserialize the ScriptModule from a file using torch::jit::load().
std::shared_ptr<torch::jit::script::Module> module = torch::jit::load(argv[1]);
assert(module != nullptr);
std::cout << "ok\n";
// Create a vector of inputs.
std::vector<torch::jit::IValue> inputs;
inputs.push_back(torch::rand({1, 1, 28, 28}));
// Execute the model and turn its output into a tensor.
auto output = module->forward(inputs).toTensor();
std::cout << output.slice(/*dim=*/1, /*start=*/0, /*end=*/10) << '\n';
}
- 結果
./example-app model.pt
ok
Columns 1 to 8-12.0483 2.1065 -2.1548 -11.6267 -6.6993 -7.9013 -12.9029 -1.5719
Columns 9 to 10-14.2974 -10.0303
[ Variable[CPUFloatType]{1,10} ]