利用C++調用Pytorch的模型

背景

  1. PyTorch的主要接口是Python語言。雖然Python是許多需要動態和易於迭代的場景的首選語言,但同樣有很多情況下,Python的這些屬性恰好是不利的。在生產環境中,需要保證低延遲和其它嚴格的要求,而對於生產場景,C++通常是首選語言,通常C++會被綁定到Java或Go當中;
  2. 第一種方法是在PyTorch中直接使用C++編寫PyTorch的前端,而不是通常情況下使用Python來編寫PyTorch的前端,實現模型定義、訓練和評估以及模型的部署;
  3. 第二種方法是使用Python編寫PyTorch的前端,並且實現上述功能;
  4. 衆所周知,Python相對於C++在不考慮執行效率的情況下具有很多優勢,本文不會討論這方面的問題。因此,如果可以使用Python編寫前端,實現模型定義、訓練和評估,而將模型的部署交由C++實現,則可以最大化目標,最快地獲得模型以及部署高效的模型。
  5. 概念轉換成具體的方案,將Python在PyTorch下訓練得到的模型文件轉化成C++可以加載和執行的模型文件,並且自此以後不再依賴於Python;
  6. PyTorch模型從Python到C ++之旅由Torch Script實現,Torch Script是PyTorch模型的一種表示,並且可以由Torch Script的編譯器理解、編譯和序列化。

環境

  1. 安裝PyTorch的版本爲1.0及以上;
  2. 安裝C++版本的LibTorch,LibTorch發行版包含一組共享庫,頭文件和CMake構建配置文件;
  3. 安裝Intel所提供的MKL-DNN庫,Caffe依賴此庫,編譯得到的可執行程序會依賴其中的libmklmllibiomp5動態鏈接庫,若無此庫在執行程序時會有錯誤產生,安裝後將這兩個動態鏈接庫拷貝至LibTorch的lib目錄下;

Mac OS 報錯信息如下:
dyld: Library not loaded: @rpath/libmklml.dylib> dyld: Library not loaded: @rpath/libmklml.dylib
Referenced from: ******
Reason: image not found

  1. 安裝CMake。

將PyTorch模型轉化爲Torch Script的兩種方法

  1. 如果需要C++使用PyTorch的模型,就必須先將PyTorch模型轉化爲Torch Script;
  2. 目前有兩種方法,可以將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

  1. 在某些情況下,如模型採用特定形式的控制流(if...else...),可以使用註釋的方法將模型轉化爲Torch Script;
  2. 此模塊的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()
  1. MyModule現在直接創建一個新對象會生成一個可以進行序列化的ScriptModule實例 。

模型序列化

不論使用了上述的哪一種方法,當ScriptModule掌握了模型的Tracing或註釋,就可以將其序列化爲文件。稍後將能夠使用C++從該文件加載模型並執行它,不需要依賴於Python。而要執行序列化,只需在模型的實例上調用save方法。

traced_script_module.save("model.pt")

現在正式離開Python的領域,並準備跨越到C ++領域。


使用C++加載腳本模塊

  1. 在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";
}
  1. <torch/script.h>是編譯運行上述代碼所需的LibTorch頭文件;
  2. 接受序列化的ScriptModule文件作爲唯一的命令行參數;
  3. 使用torch::jit::load方法反序列化文件,該方法的參數爲序列化ScriptModule的文件;
  4. 反序列化文件後返回共享所有權的智能指針,其類型爲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
  1. 父類不再是torch.nn.module,而是torch.jit.ScriptModule
  2. 使用torch.jit.trace跟蹤函數,跟蹤只能正確記錄不依賴於數據的函數和模塊,並且沒有任何未跟蹤的外部依賴(例如,執行輸入/輸出或訪問全局變量);
  3. Python函數或者模塊將使用輸入數據執行,其參數和返回值必須是Tensor或者包含Tensor的元組,函數或者模塊作爲方法的第一個參數;
  4. 在跟蹤時將輸入數據的形狀傳遞給函數作爲方法的第二個參數;
def f(x):
    return x * 2
traced_f = torch.jit.trace(f, torch.rand(1))
  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))
  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';
}
  1. 結果
./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} ]

官方文檔

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章