Bert與TensorRT部署手冊,享受絲滑的順暢
# 前言
1. 模型部署一直是我心頭的一塊刺,由于本人喜歡使用torch來做開發(fā),可想而知并沒像tf-serving這樣強力的工具來幫助我進行快速簡單的模型部署,不過筆者這個人就是比較認死理,雖然不好搞,但總得走出來一條路來
## 模型部署現有方法總結
1. 對于模型部署的方案目前比較流行的應該就幾種:
flask(django,tornado)+genius+nginx的一套組合拳。
2. 近些年比較流行的FastAPI。
3. onnxruntime + Triton inference server。
4. TensorRT + Triton inference server
# Flask與FastAPI
1. 筆者大概就收集到了這些部署的方法,flask跟fastapi的方法比較簡單,我想大部分人應該也比較熟悉這種方式,這里簡單介紹一下,本質上是HTTP服務,當有請求的時候后端進行推理返回結果進行展示,這方面資料特別多,而且相對來說非常的簡單,筆者就不重點介紹了
## ONNX與ONNXRuntime
1. onnx相信大部分torch的使用者對此應該比較熟悉它是一種針對機器學習所設計的開放式的文件格式,用于存儲訓練好的模型。它使得不同的框架可以采用相同格式存儲模型數據并交互,而微軟ONNXRuntime既是推理的一種工具,而我們的首要目標就是如何把torch保存的pth模型轉換為onnx模型并利用onnxruntime進行運行,這里筆者也進行了一些調研,發(fā)現大部分都是在CV上的工作,而NLP上的特別少,不過幸運的是huggingface官方給出了轉為onnx格式的一些方式(參考鏈接會在文末給出),不過huggingface給出的是一些pipline方法的,我們自己訓練模型的話,還是需要自己動手,既然這樣,那只能自己動手了
2. 首先我們先令保存的pth模型轉換為onnx,通過以上代碼就能把pth模型轉換為onnx了,如此絲滑簡單,而我們在進行推理的時候只需要利用onnxruntime進行加載推理即可,這里也給出代碼:
torch.onnx.export(traced_model,args = (input_ids, attention_mask, token_type_ids),f=path,opset_version=13,do_constant_folding=True,verbose=False,input_names=['input_ids', 'attention_mask', 'token_type_ids'],output_names=['output'],dynamic_axes={"input_ids": {0: "batch_size", 1: "maxlen"},"attention_mask": {0: "batch_size", 1: "maxlen"},"token_type_ids": {0: "bacth_size", 1: "maxlen"},})
3. 這樣我們就能通過onnx的方式對模型進行推理了,這樣比torch本身的方式快上不少,但我們還能不能更快,答案是肯定的。
## TensorRT
1. Nvidia的TensorRT相信大部分人也都聽說過了,這是Nvidia推出的一個高性能的深度學習推理框架,可以讓深度學習模型在NVIDIA GPU上實現低延遲,高吞吐量的部署,再結合前一段看到了出的Torch-TensorRT能極大的簡化轉換的難度,筆者決定試試水。
2. 本來筆者心里已經做好了準備,但是其中的困難卻比我想象的要復雜的多,因為沒有root權限只能通過conda的環(huán)境進行安裝,而想要安裝Torch-TensorRT你需要最低CUDA10.2的要求,記住一定要把版本對齊,不然困難非常的大,而一般公司的電腦版本都比較穩(wěn)定(落后)所以你還需要升級gcc跟g++的版本,如果沒有cmake的話需要下載進行源碼編譯,而TensorRT要上到nvidia的官方網址對應好自己的版本進行源碼編譯安裝,當然為了之后的考慮最好把pycuda也安裝上,這其中也會報各種各樣的錯誤,但都不太難解決,筆者這里選擇了最新的TensorRT8.2跟cuda11.3的版本按照官網的要求安裝好依賴,再次提醒版本一定要對,不然會出現很多問題,但是當筆者解決完這些問題的時候,準備跑過demo試一試,果然不出所料,直接報錯,我心里有大大的問號??我明明跑的官方的demo為何有錯,我定睛一看,原來是官方Blog中多了一個"]",到這里我對英偉達的官方BLOG基本已經失去信心了,不過在改掉這個小錯誤之后,跑成功了,內心狂喜,準備立馬安排我的代碼,果然又出問題了,在轉換的過程中出現(Unsupported operator: aten::Int.Tensor(Tensor a) -> (int))到這筆者已經不知道該怎么辦了,筆者在網上查找了一下,果然遇到此問題的人很多,而且并沒有得到一個解決,看來這個庫還需要完善,有時候想想可能這就是理想與現實的差距吧。
3. 既然Torch-TensorRT沒法用,那該怎么辦?我們來看看都有哪些方式轉換為TensorRT
4. 硬懟,通過nvidia提供的api構建engine,并進行推理
5. 通過onnx2tensorrt把onnx轉換為TensorRT
6. 利用Nvidia自帶工具轉換
7. Torch-TensorRT
8. Torch2trt
9. 首先第四種方法已經被我們淘汰了,解決不了,而硬懟對筆者來說還是稍微靠后一點吧,筆者這時候就是想找個簡單的來試試,那么Torch2trt就是首選,他甚至不用轉換為onnx跟Torch-TensorRT一樣一鍵轉換,但是鑒于之前的經驗,覺得事情沒那么簡單,果然又是報錯,到這里筆者已經沒什么耐心去做調試了。
10. 筆者想著既然已經轉換成onnx了,那么我們就退而求其次利用onnx2tensorrt來轉換一下不就可以了嗎,想著確實任務簡單,但往往現實就是那么的殘酷,筆者已經做好心理準備了,但是這次困難來的太快了,onnx2tensorrt需要利用Protobuf進行安裝,這里筆者想著下的是最新版本的Tensorrt,而onnx2tensorrt的要求是Protobuf >= 3.0.x就行,那么筆者下載了最新的Protobuf-3.19版本,編譯起來還是比較順利的,然后按照TensorRT的編譯要求進行編譯,注意這點的配置一定要配好,比如protobuf的路徑以及cuda的路徑,當筆者把這一切都準備編譯的的時候果然,又出問題了,glibc版本過低,那行筆者就對glibc進行升級,但是這有個問題,glibc屬于底層庫首先不太好繞開root其次如果glibc升級可能導致其他程序出問題,這么大的鍋我可背不起,最后實在是沒辦法了,只能申請運維把docker權限放給我,利用docker來構建,這下就繞開了glibc的問題,筆者通過nvidia官方下載了Tensorrt的docker,運行起來后配置了一大堆東西,然后按照剛剛的步驟重新來到onnx-tensorRT這里,這次glibc沒有報錯,但protobuf報錯了,說實話到這里筆者心態(tài)已經有點爆炸了,這方面的資料真的是少之又少,只能自己硬著頭皮搞,那么既然報錯,就從錯誤信息找,筆者找到了報錯的原因是因為
coded_input.SetTotalBytesLimit(std::numeric_limits::max(), std::numeric_limits::max() / 4)這個行代碼報錯,筆者通過提示尋到了protobuf里而在protobuf里這行代碼已經變成了只有一個參數值了,我真的是無語了,但筆者現在該選擇什么版本呢,什么時候改的我并不清楚,只能從git上下下來,然后查看歷史版本,果然前幾個月改的,那么我現在有兩個選擇第一下載前一個版本,第二修改這行代碼,沒辦法筆者這個人喜歡新的,那就只能改源碼了,筆者把cpp跟hpp文件中的代碼改成了單參數版本,這樣配置好參數make && make install之后成功安裝,準備試試轉換如何,但是正當筆者嘗試轉換的時候果然又報錯了,說實話到這筆者有點想打人了,我反思下自己,沒事瞎搞什么,但筆者不甘心,剩下的只有兩種方法,自帶工具轉換已經硬懟。
11. 這里筆者有點想硬懟了,沒辦法了,但是對自帶工具還是報了一些希望,打算先試一試吧,這一試竟然成功了,到這里筆者非常的高興,終于轉成功,但是推理的代碼還需要自己寫,沒辦法,硬來吧,這里筆者參考了torch2trt中推理的一些代碼.
class TRTModule(torch.nn.Module):def __init__(self, engine=None, input_names=None, output_names=None):super(TRTModule, self).__init__()# self._register_state_dict_hook(TRTModule._on_state_dict)self.engine = engineif self.engine is not None:self.context = self.engine.create_execution_context()self.input_names = input_namesself.output_names = output_namesdef forward(self, *inputs):batch_size = inputs[0].shape[0]bindings = [None] * (len(self.input_names) + len(self.output_names))# create output tensorsoutputs = [None] * len(self.output_names)for i, output_name in enumerate(self.output_names):idx = self.engine.get_binding_index(output_name)# c = inputs[i].shapedtype = torch_dtype_from_trt(self.engine.get_binding_dtype(idx))shape = tuple(self.engine.get_binding_shape(idx))device = torch_device_from_trt(self.engine.get_location(idx))output = torch.empty(size=shape, dtype=dtype, device=device)outputs[i] = outputbindings[idx] = output.data_ptr()for i, input_name in enumerate(self.input_names):idx = self.engine.get_binding_index(input_name)self.context.set_binding_shape(idx, tuple(inputs[i].shape))bindings[idx] = inputs[i].contiguous().data_ptr()self.context.execute_async(batch_size, bindings, torch.cuda.current_stream().cuda_stream)outputs = tuple(outputs)if len(outputs) == 1:outputs = outputs[0]return outputs
1. 正準備推理時,果然,又報錯了,哎~說實話已經有些習慣了,就像被老板反復按在地上摩擦,但你又不得不干的時候,習慣真是可怕的力量,有問題就只能解決,原來我的onnx中運用了dynamic_axes動態(tài)輸入的參數,那么現在有兩種方案來解決,第一.指定為靜態(tài)shape,第二.TensorRT轉換為動態(tài)輸入,都已經到這了,筆者當然會選擇動態(tài)輸入了,但是要怎么做,只能看官方文檔,文章中指出需要指定動態(tài)輸入的三個參數minShapes,optShapes與maxShapes,當你指定好之后順利轉換成功,然后運行剛剛寫好的推理代碼,哈哈~,再次失敗。。不過這次的問題很好解決就是筆者的torch的cuda版本不對,當與cuda11.3對齊過后順利得到了結果。
2. 到這里筆者搞了將近一個月的Tensort算是完成了,剩下的就是把Tensorrt利用Triton inference server進行推理,以及一些推理過程的比較,這個下一篇文章再寫吧,哎,這一篇一篇文章最后都留下了一些坑沒填,本想填坑,但業(yè)務需求一直在變,也沒騰出手來做一些事情,等到一部分事情做完的時候,就慢慢的把坑填起來,忽然想起筆者這里還有一篇之前做的關于Prompt的實驗還沒有寫文章,這個就準備準備也分享一下了~
