經驗 | 在C++平臺上部署PyTorch模型流程+踩坑實錄

2022-02-05 計算機視覺聯盟

點上方藍字計算機視覺聯盟獲取更多乾貨

來源丨https://zhuanlan.zhihu.com/p/146453159

最近因為工作需要,要把pytorch的模型部署到c++平臺上,基本過程主要參照官網的教學示例,期間發現了不少坑,特此記錄。

libtorch不依賴於python,python訓練的模型,需要轉換為script model才能由libtorch加載,並進行推理。在這一步官網提供了兩種方法:

方法一:Tracing

這種方法操作比較簡單,只需要給模型一組輸入,走一遍推理網絡,然後由torch.ji.trace記錄一下路徑上的信息並保存即可。示例如下:

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)

缺點是如果模型中存在控制流比如if-else語句,一組輸入只能遍歷一個分支,這種情況下就沒辦法完整的把模型信息記錄下來。

方法二:Scripting

直接在Torch腳本中編寫模型並相應地注釋模型,通過torch.jit.script編譯模塊,將其轉換為ScriptModule。示例如下:

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)

forward方法會被默認編譯,forward中被調用的方法也會按照被調用的順序被編譯

如果想要編譯一個forward以外且未被forward調用的方法,可以添加 @torch.jit.export.

如果想要方法不被編譯,可使用

@torch.jit.ignore(https://pytorch.org/docs/master/generated/torch.jit.ignore.html#torch.jit.ignore) 

或者 @torch.jit.unused(https://pytorch.org/docs/master/generated/torch.jit.unused.html#torch.jit.unused)

# Same behavior as pre-PyTorch 1.2
@torch.jit.script
def some_fn():
return 2

# Marks a function as ignored, if nothing
# ever calls it then this has no effect
@torch.jit.ignore
def some_fn2():
return 2

# As with ignore, if nothing calls it then it has no effect.
# If it is called in script it is replaced with an exception.
@torch.jit.unused
def some_fn3():
import pdb; pdb.set_trace()
return 4

# Doesn't do anything, this function is already
# the main entry point
@torch.jit.export
def some_fn4():
return 2

在這一步遇到好多坑,主要原因可歸為一下兩點

1. 不支持的操作

TorchScript支持的操作是python的子集,大部分torch中用到的操作都可以找到對應實現,但也存在一些尷尬的不支持操作,詳細列表可見https://pytorch.org/docs/master/jit_unsupported.html#jit-unsupported,下面列一些我自己遇到的操作:

1)參數/返回值不支持可變個數,例如

def __init__(self, **kwargs):

或者

if output_flag == 0:
return reshape_logits
else:
loss = self.loss(reshape_logits, term_mask, labels_id)
return reshape_logits, loss

2)各種iteration操作

eg1.

layers = [int(a) for a in layers]

報錯torch.jit.frontend.UnsupportedNodeError: ListComp aren’t supported

可以改成:

for k in range(len(layers)):
layers[k] = int(layers[k])

eg2.

seq_iter = enumerate(scores)
try:
_, inivalues = seq_iter.__next__()
except:
_, inivalues = seq_iter.next()

eg3.

line = next(infile)

3)不支持的語句

eg1. 不支持continue

torch.jit.frontend.UnsupportedNodeError: continue statements aren’t supported

eg2. 不支持try-catch

torch.jit.frontend.UnsupportedNodeError: try blocks aren’t supported

eg3. 不支持with語句

4)其他常見op/module

eg1. torch.autograd.Variable

解決:使用torch.ones/torch.randn等初始化+.float()/.long()等指定數據類型。

eg2. torch.Tensor/torch.LongTensor etc.

解決:同上

eg3. requires_grad參數只在torch.tensor中支持,torch.ones/torch.zeros等不可用

eg4. tensor.numpy()

eg5. tensor.bool()

解決:tensor.bool()用tensor>0代替

eg6. self.seg_emb(seg_fea_ids).to(embeds.device)

解決:需要轉gpu的地方顯示調用.cuda()

總之一句話:除了原生python和pytorch以外的庫,比如numpy什麼的能不用就不用,儘量用pytorch的各種API。

2. 指定數據類型

1)屬性,大部分的成員數據類型可以根據值來推斷,空的列表/字典則需要預先指定

from typing import Dict

class MyModule(torch.nn.Module):
my_dict: Dict[str, int]

def __init__(self):
super(MyModule, self).__init__()
# This type cannot be inferred and must be specified
self.my_dict = {}

# The attribute type here is inferred to be `int`
self.my_int = 20

def forward(self):
pass

m = torch.jit.script(MyModule())

2)常量,使用Final關鍵字

try:
from typing_extensions import Final
except:
# If you don't have `typing_extensions` installed, you can use a
# polyfill from `torch.jit`.
from torch.jit import Final

class MyModule(torch.nn.Module):

my_constant: Final[int]

def __init__(self):
super(MyModule, self).__init__()
self.my_constant = 2

def forward(self):
pass

m = torch.jit.script(MyModule())

3)變量。默認是tensor類型且不可變,所以非tensor類型必須要指明

def forward(self, batch_size:int, seq_len:int, use_cuda:bool):

方法三:Tracing and Scriptin混合

一種是在trace模型中調用script,適合模型中只有一小部分需要用到控制流的情況,使用實例如下:

import torch

@torch.jit.script
def foo(x, y):
if x.max() > y.max():
r = x
else:
r = y
return r


def bar(x, y, z):
return foo(x, y) + z

traced_bar = torch.jit.trace(bar, (torch.rand(3), torch.rand(3), torch.rand(3)))

另一種情況是在script module中用tracing生成子模塊,對於一些存在script module不支持的python feature的layer,就可以把相關layer封裝起來,用trace記錄相關layer流,其他layer不用修改。使用示例如下:

import torch
import torchvision

class MyScriptModule(torch.nn.Module):
def __init__(self):
super(MyScriptModule, self).__init__()
self.means = torch.nn.Parameter(torch.tensor([103.939, 116.779, 123.68])
.resize_(1, 3, 1, 1))
self.resnet = torch.jit.trace(torchvision.models.resnet18(),
torch.rand(1, 3, 224, 224))

def forward(self, input):
return self.resnet(input - self.means)

my_script_module = torch.jit.script(MyScriptModule())

如果上一步的坑都踩完,那麼模型保存就非常簡單了,只需要調用save並傳遞一個文件名即可,需要注意的是如果想要在gpu上訓練模型,在cpu上做inference,一定要在模型save之前轉化,再就是記得調用model.eval(),形如

gpu_model.eval()
cpu_model = gpu_model.cpu()
sample_input_cpu = sample_input_gpu.cpu()
traced_cpu = torch.jit.trace(traced_cpu, sample_input_cpu)
torch.jit.save(traced_cpu, "cpu.pth")

traced_gpu = torch.jit.trace(traced_gpu, sample_input_gpu)
torch.jit.save(traced_gpu, "gpu.pth")

要在C ++中加載序列化的PyTorch模型,必須依賴於PyTorch C ++ API(也稱為LibTorch)。libtorch的安裝非常簡單,只需要在pytorch官網(https://pytorch.org/)下載對應版本,解壓即可。會得到一個結構如下的文件夾。

libtorch/
bin/
include/
lib/
share/

然後就可以構建應用程式了,一個簡單的示例目錄結構如下:

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

example-app.cpp和CMakeLists.txt的示例代碼分別如下:

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

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 14)

至此,就可以運行以下命令從example-app/文件夾中構建應用程式啦:

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

其中/path/to/libtorch是之前下載後的libtorch文件夾所在的路徑。這一步如果順利能夠看到編譯完成100%的提示,下一步運行編譯生成的可執行文件,會看到「ok」的輸出,可喜可賀!

終於到最後一步啦!下面只需要按照構建輸入傳給模型,執行forward就可以得到輸出啦。一個簡單的示例如下:

// 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的向量,並添加單個輸入. 使用torch::ones()創建輸入張量,等效於C ++ API中的torch.ones。然後,運行script::Module的forward方法,通過調用toTensor()將返回的IValue值轉換為張量。C++對torch的各種操作還是比較友好的,通過torch::或者後加_的方法都可以找到對應實現,例如

torch::tensor(input_list[j]).to(at::kLong).resize_({batch, 128}).clone()
//torch::tensor對應pytorch的torch.tensor; at::kLong對應torch.int64;resize_對應resize

最後check一下確保c++端的輸出和pytorch是一致的就大功告成啦~

踩了無數坑,薅掉了無數頭髮,很多東西也是自己一點點摸索的,如果有錯誤歡迎指正!

PyTorch C++ API - PyTorch master document

Torch Script - PyTorch master documentation

文章地址:

https://pytorch.org/cppdocs/

https://pytorch.org/tutorials/advanced/cpp_export.html

這是我的私人微信,還有少量坑位,可與相關學者研究人員交流學習 目前開設有人工智慧、機器學習、計算機視覺、自動駕駛(含SLAM)、Python、求職面經、綜合交流群掃描添加CV聯盟微信拉你進群,備註:CV聯盟

博士筆記 | 周志華《機器學習》手推筆記第一章思維導圖

博士筆記 | 周志華《機器學習》手推筆記第九章

點個在看支持一下吧

相關焦點

  • 模型部署翻車記:PyTorch轉onnx踩坑實錄
    在深度學習模型部署時,從pytorch轉換onnx的過程中,踩了一些坑。本文總結了這些踩坑記錄,希望可以幫助其他人。首先,簡單說明一下pytorch轉onnx的意義。在pytorch訓練出一個深度學習模型後,需要在TensorRT或者openvino部署,這時需要先把Pytorch模型轉換到onnx模型之後再做其它轉換。因此,在使用pytorch訓練深度學習模型完成後,在TensorRT或者openvino或者opencv和onnxruntime部署時,pytorch模型轉onnx這一步是必不可少的。
  • 輕鬆學pytorch之使用onnx runtime實現pytorch模型部署
    ONNX(Open Neural Network Exchange)是一種標準與開放的網絡模型交換格式,直白點說就是tensorflow/pytorch
  • C++實現yolov5的OpenVINO部署
    [GiantPandaCV導語] 本文介紹了一種使用c++實現的,使用OpenVINO部署yolov5的方法。此方法在2020年9月結束的極市開發者榜單中取得後廚老鼠識別賽題第四名。2020年12月,注意到yolov5有了許多變化,對部署流程重新進行了測試,並進行了整理。希望能給需要的朋友一些參考,節省一些踩坑的時間。模型訓練1.
  • TensorRT加速PyTorch模型教程
    這個貼子只涉及pytorch,對於tensorflow的話,可以參考TensorRT部署深度學習模型,https://zhuanlan.zhihu.com/p/84125533,這個帖子是c++如何部署TensorRT。
  • 訓練好的深度學習模型原來這樣部署的!(乾貨滿滿,收藏慢慢看)
    ,切到test模式,拿python跑一跑就好,順手寫個簡單的GUI展示結果;高級一點,可以用CPython包一層接口,然後用C++工程去調用需求二:要放到伺服器上去跑,不要求吞吐和時延caffe、tf、pytorch等框架隨便選一個,按照官方的部署教程,老老實實用C++部署,例如pytorch模型用工具導到libtorch下跑。
  • TensorRT 加速 PyTorch 模型基本方法
    這個貼子只涉及pytorch,對於tensorflow的話,可以參考TensorRT部署深度學習模型,https://zhuanlan.zhihu.com/p/84125533,這個帖子是c++如何部署TensorRT。
  • 用pytorch踩過的坑
    ,因為要監控模型的性能,在跑完若干個epoch訓練之後,需要進行一次在驗證集[4]上的性能驗證。保存梯度是需要額外顯存或者內存進行保存的,佔用了空間,有時候還會在驗證階段導致OOM(Out Of Memory)錯誤,因此我們在驗證和測試階段,最好顯式地取消掉模型變量的梯度。
  • 用PyTorch部署模型
    的更多信息:https://github.com/pytorch/serve#quick-start-with-dockerHandlers官方文檔:https://github.com/pytorch/serve/blob/master/docs/custom_service.md處理程序負責使用模型對一個或多個
  • 如何使用TensorRT對訓練好的PyTorch模型進行加速?
    這個貼子只涉及pytorch,對於tensorflow的話,可以參考TensorRT部署深度學習模型,https://zhuanlan.zhihu.com/p/84125533,這個帖子是c++如何部署TensorRT。
  • 那些用pytorch踩過的坑
    ,因為要監控模型的性能,在跑完若干個epoch訓練之後,需要進行一次在驗證集[4]上的性能驗證。保存梯度是需要額外顯存或者內存進行保存的,佔用了空間,有時候還會在驗證階段導致OOM(Out Of Memory)錯誤,因此我們在驗證和測試階段,最好顯式地取消掉模型變量的梯度。
  • TensorRT模型加速部署方案解析(視頻/案例講解)
    這個課程主要圍繞著https://github.com/shouxieai/tensorRT_cpp提供的方案開展討論,使得能夠使用、部署起來該教程,講駕馭tensorRT,實現從模型導出,到c++/python推理加速,再到高性能、低耦合、有效、便捷的工程落地方案以最終可用、好用為出發點需要的知識點:1、對深度學習的認識,CV相關知識
  • 數據挖掘:聊聊那些年你我踩過的「坑」
    今天就來談一談數據挖掘中常常被我們忽略的小問題(踩過的坑)。讓我們從下面這張圖開始吧! 圖一 從現實世界到模型世界(圖片來自網絡) 咳咳注意,本篇不是八卦文,在這裡我們要正經地討論一些小case。如圖所示,我們以左圖代表現實世界,右圖代表模型世界——對,數據挖掘的世界。
  • torchserve來進行PyTorch模型的部署
    的更多信息:https://github.com/pytorch/serve#quick-start-with-dockerHandlers官方文檔:https://github.com/pytorch/serve/blob/master/docs/custom_service.md處理程序負責使用模型對一個或多個
  • 一文詳解pytorch的「動態圖」與「自動微分」技術
    想要獲取中間變量的輸出,可以是可以,就是比較麻煩一些,caffe使用c++訓練的話,需要獲取layer的top,然後列印,tensorflow需要通過session來獲取。但是如果想要控制網絡的運行,比如讓網絡停在某一個OP之後,這是很難做到的。即無法精確的控制網絡運行的每一步,只能等網絡運行完了,然後通過相關的接口去獲取相關的數據。而pytorch的「動態圖」機制就可以對網絡實現非常精確的控制。
  • onnx實現對pytorch模型推理加速
    向AI轉型的程式設計師都關注了這個號👇👇👇人工智慧大數據與深度學習  公眾號:datayx微軟宣布將多平臺通用ONNX Runtime是適用於Linux,Windows和Mac上ONNX格式的機器學習模型的高性能推理引擎。
  • PyTorch OCR模型的安卓端部署
    來源 | GiantPandaCV 作者 | 阿呆本文作業系統為Windows,因為Windows上的安卓模擬器選擇較多,並且真機調試也比較方便;交叉編譯在Windows和Ubuntu上都進行了嘗試,都可行,但是如果是Ubuntu上交叉編譯之後再挪到
  • 踩坑|NVIDIA驅動安裝
    我也不知道伺服器具體有哪些毛病,不如先跑一個複雜點的代碼踩踩坑,比如我這個是pytorch下,利用多個GPU並行的圖形識別訓練程序。基本上通過跑這個程序,可以排查出伺服器的大部分問題。cuda和cudnn的安裝也是個麻煩事,這裡就先不寫了,放到明天吧。本文主要寫重新安裝NVIDIA驅動。
  • 驚天地泣鬼神的DETR轉TensorRT加速過程實錄
    很久沒有更新原創文章了,今天這一篇一定要更新一下,因為我踩的坑實在是太多了.我做的事情是將DETR轉到TensorRT進行加速,希望能收穫Transformer目標檢測方法更快部署的工作.前段時間像Efficient-DETR,Anchor-DETR等transformer-based的目標檢測文章相繼推出,讓我不得不對這個方向跟進一波,然後我就發現這類方法部署的痛點: 儘管模型架構很簡單
  • 模型推理加速系列|如何用ONNX加速BERT特徵抽取(附代碼)
    小夥伴們都知道,BERT體系模型龐(臃)大(腫),各種刷榜自然是香,但是在工業落地上的推理性能一直是道坎。如何高效提升NLP,在今年的EMNLP2020上有一場tutorial非常廣泛地介紹了各種提高NLP效率的奇技淫巧,感興趣的可以☛戳這裡。
  • 實用教程詳解:模型部署,用DNN模塊部署YOLOv5目標檢測(附原始碼)
    在典型的機器學習和深度學習項目中,我們通常從定義問題陳述開始,然後是數據收集和準備(數據預處理)和模型構建(模型訓練),對吧?但是,最後,我們希望我們的模型能夠提供給最終用戶,以便他們能夠利用它。模型部署是任何機器學習項目的最後階段之一,可能有點棘手。如何將機器學習模型傳遞給客戶/利益相關者?