TensorRT詳細(xì)入門(mén)指北踏兜,如果你還不了解TensorRT词顾,過(guò)來(lái)看看吧八秃!

前言

大名鼎鼎的TensorRT有多牛逼就不多說(shuō)了,因?yàn)榇_實(shí)很好用计技。

作為在英偉達(dá)自家GPU上的推理庫(kù)喜德,這些年來(lái)一直被大力推廣,更新也非常頻繁垮媒,issue反饋也挺及時(shí)舍悯,社區(qū)的負(fù)責(zé)人員也很積極,簡(jiǎn)直不要太NICE睡雇。

只是TensorRT的入門(mén)門(mén)檻略微高一點(diǎn)點(diǎn)萌衬,勸退了一部分玩家。一部分原因是官方文檔也不夠詳細(xì)(其實(shí)也挺細(xì)了它抱,只不過(guò)看起來(lái)有些雜亂)秕豫、資料不夠;另一部分可能是因?yàn)門(mén)ensorRT比較底層观蓄,需要一點(diǎn)點(diǎn)C++和硬件方面的知識(shí)混移,相較學(xué)習(xí)Python難度更大一點(diǎn)。

不過(guò)吐槽歸吐槽侮穿,TensorRT官方文檔依舊是最權(quán)威最實(shí)用的查閱手冊(cè)歌径,另外TensorRT也是全面支持Python的,不習(xí)慣用C++的小伙伴亲茅,用Python調(diào)用TensorRT是沒(méi)有任何問(wèn)題的回铛。

你懂得

一直也想寫(xiě)TensorRT的系列教程,也順便整理一下自己之前做的筆記克锣。拖了好久的TensorRT入門(mén)指北終于寫(xiě)完了翅萤。

本教程基于目前(2021-4-26)最新版TensorRT-7.2.3.4如捅,TensorRT更新頻繁淆衷,TensorRT-8可能不久也會(huì)發(fā)布捂贿,不過(guò)TensorRT對(duì)于向下兼容的API做的還是比較好的,不必?fù)?dān)心太多的遷移問(wèn)題榕酒。

今天簡(jiǎn)單一刷(2021-5-5)胚膊,TensorRT終于發(fā)布8版本了,仍然是嘗鮮的Early Access (EA)想鹰,官方release紊婉。不過(guò)TensorRT8-EA版一些功能不是很穩(wěn)定,我們先不著急使用辑舷。嘗鮮的小伙伴們可以先試試喻犁,生產(chǎn)環(huán)境版本建議先不要?jiǎng)印?/p>

之前老潘也寫(xiě)過(guò)一些關(guān)于TensorRT文章,其中的部分內(nèi)容也會(huì)整合到這一系列中,方便查閱:

不廢話了剩盒,直接開(kāi)始吧~

附上TensorRT官方文檔鏈接。

什么是TensorRT

TensorRT是可以在NVIDIA各種GPU硬件平臺(tái)下運(yùn)行的一個(gè)C++推理框架慨蛙。我們利用Pytorch辽聊、TF或者其他框架訓(xùn)練好的模型,可以轉(zhuǎn)化為T(mén)ensorRT的格式期贫,然后利用TensorRT推理引擎去運(yùn)行我們這個(gè)模型跟匆,從而提升這個(gè)模型在英偉達(dá)GPU上運(yùn)行的速度。速度提升的比例是比較可觀的通砍。

借官方的話來(lái)說(shuō):

The core of NVIDIA? TensorRT? is a C++ library that facilitates high-performance inference on NVIDIA graphics processing units (GPUs). TensorRT takes a trained network, which consists of a network definition and a set of trained parameters, and produces a highly optimized runtime engine that performs inference for that network.
TensorRT provides API's via C++ and Python that help to express deep learning models via the Network Definition API or load a pre-defined model via the parsers that allow TensorRT to optimize and run them on an NVIDIA GPU. TensorRT applies graph optimizations, layer fusion, among other optimizations, while also finding the fastest implementation of that model leveraging a diverse collection of highly optimized kernels. TensorRT also supplies a runtime that you can use to execute this network on all of NVIDIA’s GPU’s from the Kepler generation onwards.
TensorRT also includes optional high speed mixed precision capabilities introduced in the Tegra? X1, and extended with the Pascal?, Volta?, Turing?, and NVIDIA? Ampere GPU architectures.

TensorRT支持的平臺(tái)如下:

TensorRT支持的硬件平臺(tái)

支持計(jì)算能力在5.0及以上的顯卡(當(dāng)然玛臂,這里的顯卡可以是桌面級(jí)顯卡也可以是嵌入版式顯卡),我們常見(jiàn)的RTX30系列計(jì)算能力是8.6封孙、RTX20系列是7.5迹冤、RTX10系列是6.1,如果我們想要使用TensorRT虎忌,首先要確認(rèn)下我們的顯卡是否支持叁巨。

至于什么是計(jì)算能力(Compute Capability),咋說(shuō)嘞呐籽,計(jì)算能力并不是描述GPU設(shè)備計(jì)算能力強(qiáng)弱的絕對(duì)指標(biāo),準(zhǔn)確的說(shuō)蚀瘸,這是一個(gè)架構(gòu)的版本號(hào)狡蝶。一般來(lái)說(shuō)越新的架構(gòu)版本號(hào)更高,計(jì)算能力的第一個(gè)數(shù)值也就最高(例如3080計(jì)算能力8.6)贮勃,而后面的6代表在該架構(gòu)前提下的一些優(yōu)化特性贪惹,具體代表什么特性可以看這里:

可以通過(guò)這里查閱自己顯卡的計(jì)算能力。

說(shuō)回TensorRT本身寂嘉,TensorRT是由C++奏瞬、CUDA、python三種語(yǔ)言編寫(xiě)成的一個(gè)庫(kù)泉孩,其中核心代碼為C++和CUDA硼端,Python端作為前端與用戶交互。當(dāng)然寓搬,TensorRT也是支持C++前端的珍昨,如果我們追求高性能,C++前端調(diào)用TensorRT是必不可少的。

TensorRT支持的模型以及硬件平臺(tái)

使用TensorRT的場(chǎng)景

TensorRT的使用場(chǎng)景很多镣典。服務(wù)端兔毙、嵌入式端、家用電腦端都是我們的使用場(chǎng)景兄春。

  • 服務(wù)端對(duì)應(yīng)的顯卡型號(hào)為A100澎剥、T4、V100等
  • 嵌入式端對(duì)應(yīng)的顯卡為AGX Xavier赶舆、TX2哑姚、Nano等
  • 家用電腦端對(duì)應(yīng)的顯卡為3080、2080TI涌乳、1080TI等

當(dāng)然這不是固定的蜻懦,只要我們顯卡滿足TensorRT的先決條件,用就對(duì)了夕晓。

TensorRT的加速效果怎么樣

加速效果取決于模型的類(lèi)型和大小宛乃,也取決于我們所使用的顯卡類(lèi)型。

對(duì)于GPU來(lái)說(shuō)蒸辆,因?yàn)榈讓拥挠布O(shè)計(jì)征炼,更適合并行計(jì)算也更喜歡密集型計(jì)算。TensorRT所做的優(yōu)化也是基于GPU進(jìn)行優(yōu)化躬贡,當(dāng)然也是更喜歡那種一大塊一大塊的矩陣運(yùn)算谆奥,盡量直通到底。因此對(duì)于通道數(shù)比較多的卷積層和反卷積層拂玻,優(yōu)化力度是比較大的酸些;如果是比較繁多復(fù)雜的各種細(xì)小op操作(例如reshape、gather檐蚜、split等)魄懂,那么TensorRT的優(yōu)化力度就沒(méi)有那么夸張了。

為了更充分利用GPU的優(yōu)勢(shì)闯第,我們?cè)谠O(shè)計(jì)模型的時(shí)候市栗,可以更加偏向于模型的并行性,因?yàn)橥瑯拥挠?jì)算量咳短,“大而整”的GPU運(yùn)算效率遠(yuǎn)超“小而碎”的運(yùn)算填帽。

工業(yè)界更喜歡簡(jiǎn)單直接的模型和backbone。2020年的RepVGG(RepVGG:極簡(jiǎn)架構(gòu)咙好,SOTA性能篡腌,讓VGG式模型再次偉大(CVPR-2021)),就是為GPU和專(zhuān)用硬件設(shè)計(jì)的高效模型敷扫,追求高速度哀蘑、省內(nèi)存诚卸,較少關(guān)注參數(shù)量和理論計(jì)算量。相比resnet系列绘迁,更加適合充當(dāng)一些檢測(cè)模型或者識(shí)別模型的backbone合溺。

在實(shí)際應(yīng)用中,老潘也簡(jiǎn)單總結(jié)了下TensorRT的加速效果:

  • SSD檢測(cè)模型缀台,加速3倍(Caffe)
  • CenterNet檢測(cè)模型棠赛,加速3-5倍(Pytorch)
  • LSTM、Transformer(細(xì)op)膛腐,加速0.5倍-1倍(TensorFlow)
  • resnet系列的分類(lèi)模型睛约,加速3倍左右(Keras)
  • GAN、分割模型系列比較大的模型哲身,加速7-20倍左右(Pytorch)

TensorRT有哪些黑科技

為什么TensorRT能夠提升我們模型在英偉達(dá)GPU上運(yùn)行的速度辩涝,當(dāng)然是做了很多對(duì)提速有增益的優(yōu)化:

TensorRT應(yīng)用的優(yōu)化
  • 算子融合(層與張量融合):簡(jiǎn)單來(lái)說(shuō)就是通過(guò)融合一些計(jì)算op或者去掉一些多余op來(lái)減少數(shù)據(jù)流通次數(shù)以及顯存的頻繁使用來(lái)提速
  • 量化:量化即IN8量化或者FP16以及TF32等不同于常規(guī)FP32精度的使用,這些精度可以顯著提升模型執(zhí)行速度并且不會(huì)保持原先模型的精度
  • 內(nèi)核自動(dòng)調(diào)整:根據(jù)不同的顯卡構(gòu)架勘天、SM數(shù)量怔揩、內(nèi)核頻率等(例如1080TI和2080TI),選擇不同的優(yōu)化策略以及計(jì)算方式脯丝,尋找最合適當(dāng)前構(gòu)架的計(jì)算方式
  • 動(dòng)態(tài)張量顯存:我們都知道商膊,顯存的開(kāi)辟和釋放是比較耗時(shí)的,通過(guò)調(diào)整一些策略可以減少模型中這些操作的次數(shù)宠进,從而可以減少模型運(yùn)行的時(shí)間
  • 多流執(zhí)行:使用CUDA中的stream技術(shù)晕拆,最大化實(shí)現(xiàn)并行操作

TensorRT的這些優(yōu)化策略代碼雖然是閉源的,但是大部分的優(yōu)化策略我們或許也可以猜到一些材蹬,也包括TensorRT官方公布出來(lái)的一些優(yōu)化策略:

TensorRT對(duì)層所做的融合操作

左上角是原始網(wǎng)絡(luò)(googlenet)实幕,右上角相對(duì)原始層進(jìn)行了垂直優(yōu)化,將conv+bias(BN)+relu進(jìn)行了融合優(yōu)化堤器;而右下角進(jìn)行了水平優(yōu)化茬缩,將所有1x1的CBR融合成一個(gè)大的CBR;左下角則將concat層直接去掉吼旧,將contact層的輸入直接送入下面的操作中,不用單獨(dú)進(jìn)行concat后在輸入計(jì)算未舟,相當(dāng)于減少了一次傳輸吞吐圈暗;

等等等等,還有很多例子裕膀。

上述的這些算子融合员串、動(dòng)態(tài)顯存分配、精度校準(zhǔn)昼扛、多steam流寸齐、自動(dòng)調(diào)優(yōu)等操作欲诺,TensorRT都幫你做了。這樣通過(guò)TensorRT幫你調(diào)優(yōu)模型后渺鹦,自然模型的速度就上來(lái)了扰法。

當(dāng)然也有其他在NVIDIA-GPU平臺(tái)上的推理優(yōu)化庫(kù),例如TVM毅厚,某些情況下TVM比TensorRT要好用些塞颁,但畢竟是英偉達(dá)自家產(chǎn)品,TensorRT在自家GPU上還是有不小的優(yōu)勢(shì)吸耿,做到了開(kāi)箱即用祠锣,上手程度不是很難。

安裝TensorRT咽安!

安裝TensorRT的方式有很多伴网,官方提供了多種方式:

You can choose between the following installation options when installing TensorRT; Debian or RPM packages, a pip wheel file, a tar file, or a zip file.

這些安裝包都可以從官方直接下載,從 https://developer.nvidia.com/zh-cn/tensorrt 進(jìn)入下載即可妆棒,需要注意這里我們要注冊(cè)會(huì)員并且登錄才可以下載澡腾。老潘一直使用的方式是下載tar包,下載好后解壓即可募逞,只要我們的環(huán)境符合要求就可以直接運(yùn)行蛋铆,類(lèi)似于綠色免安裝

例如下載TensorRT-7.2.3.4.Ubuntu-18.04.x86_64-gnu.cuda-11.1.cudnn8.1.tar.gz放接,下載好后刺啦,tar -zxvf解壓即可。

解壓之后我們需要添加環(huán)境變量纠脾,以便讓我們的程序能夠找到TensorRT的libs玛瘸。

vim ~/.bashrc
# 添加以下內(nèi)容
export LD_LIBRARY_PATH=/path/to/TensorRT-7.2.3.4/lib:$LD_LIBRARY_PATH
export LIBRARY_PATH=/path/to/TensorRT-7.2.3.4/lib::$LIBRARY_PATH

這樣TensorRT就安裝好了,很快吧苟蹈!

TensorRT常見(jiàn)FAQ

對(duì)于使用TensorRT糊渊,還是有一些需要注意的地方,老潘在這里總結(jié)了一些大家可能感興趣或者之后可能遇到的一些問(wèn)題慧脱。

TensorRT版本相關(guān)

TensorRT的版本與CUDA還有CUDNN版本是密切相關(guān)的渺绒,我們從官網(wǎng)下載TensorRT的時(shí)候應(yīng)該就可以注意到:

選擇合適自己的版本

不匹配版本的cuda以及cudnn是無(wú)法和TensorRT一起使用的。

所以下載的時(shí)候要注意菱鸥,不要搞錯(cuò)版本了哦宗兼。

關(guān)于如何選擇合適自己的TensorRT版本,首先看驅(qū)動(dòng)氮采,其次看CUDA版本殷绍。

驅(qū)動(dòng)怎么看,使用nvidia-smi命令即可:

查看自己的驅(qū)動(dòng)版本以及cuda版本

驅(qū)動(dòng)可以通過(guò)root權(quán)限去換鹊漠,而CUDA版本只要驅(qū)動(dòng)滿足要求就可以隨便換(不需要root權(quán)限)主到,這點(diǎn)注意下就好茶行。

關(guān)于詳細(xì)的環(huán)境配置可以看老潘之前的這篇文章:主機(jī)回來(lái)以及,簡(jiǎn)單的環(huán)境配置(RTX3070+CUDA11.1+CUDNN+TensorRT)

耍個(gè)小聰明登钥,其實(shí)版本也不是嚴(yán)格限制的畔师,只要是你需要的功能函數(shù)在這個(gè)低版本中存在,那也是可以使用的怔鳖。

舉個(gè)例子茉唉。

我們從官方下載的TensorRT-7.0.0.11.Ubuntu-16.04.x86_64-gnu.cuda-10.2.cudnn7.6.tar依賴(lài)libcudnn.so.7.6.0。但我們使用libcudnn.so.7.3.0去跑這個(gè)TensorRT去做一些事情時(shí)结执,因?yàn)榘姹静灰恢戮蜁?huì)報(bào)錯(cuò):

TensorRT-7.0.0.11/lib/libmyelin.so.1: undefined reference to `cudnnGetBatchNormalizationBackwardExWorkspaceSize@libcudnn.so.7'
TensorRT-7.0.0.11/lib/libmyelin.so.1: undefined reference to `cudnnGetBatchNormalizationForwardTrainingExWorkspaceSize@libcudnn.so.7'
TensorRT-7.0.0.11/lib/libmyelin.so.1: undefined reference to `cudnnGetBatchNormalizationTrainingExReserveSpaceSize@libcudnn.so.7'
TensorRT-7.0.0.11/lib/libmyelin.so.1: undefined reference to `cudnnBatchNormalizationBackwardEx@libcudnn.so.7'
TensorRT-7.0.0.11/lib/libmyelin.so.1: undefined reference to `cudnnBatchNormalizationForwardTrainingEx@libcudnn.so.7'

顯然在鏈接TensorRT的時(shí)候度陆,libmyelin.so.1這個(gè)東西需要的符號(hào)表在libcudnn.so.7.3.0這里找不到,因此也無(wú)法編譯成功献幔。

但是如果我們使用libcudnn.so.7.6.0將其編譯好得到可執(zhí)行文件懂傀,但跑這個(gè)程序時(shí)只給它提供libcudnn.so.7.3.0的運(yùn)行環(huán)境。那么運(yùn)行的時(shí)候會(huì)有
TensorRT was linked against cuDNN 7.6.3 but loaded cuDNN 7.3.0的提示蜡感,不過(guò)程序可以正常運(yùn)行蹬蚁!

原因很簡(jiǎn)單,libcudnn.so.7.6.0所擁有的cudnnGetBatchNormalizationBackwardExWorkspaceSize雖然libcudnn.so.7.3.0沒(méi)有郑兴,但是我們也不用它犀斋,所以程序可以正常運(yùn)行而不會(huì)報(bào)錯(cuò),但如果你需要這個(gè)函數(shù)調(diào)用這個(gè)函數(shù)那么就沒(méi)有辦法了情连。

通過(guò)strings命令觀察可以看到libcudnn.so.7.3.0確實(shí)沒(méi)有cudnnGetBatchNormalizationBackwardExWorkspaceSize這個(gè)函數(shù)實(shí)現(xiàn):

strings /usr/local/cuda/lib64/libcudnn.so.7.3.0 | grep cudnnGetBatchNormalizationBackwardExWorkspaceSize
而7.6.5中有
strings /usr/local/cuda/lib64/libcudnn.so.7.6.5 | grep cudnnGetBatchNormalizationBackwardExWorkspaceSize
cudnnGetBatchNormalizationBackwardExWorkspaceSize
cudnnGetBatchNormalizationBackwardExWorkspaceSize

更多關(guān)于動(dòng)態(tài)鏈接庫(kù)的細(xì)節(jié)可以(收藏必看系列)咱不知道的一些動(dòng)態(tài)鏈接庫(kù)小細(xì)節(jié)叽粹。

什么模型可以轉(zhuǎn)換為T(mén)ensorRT

TensorRT官方支持Caffe、Tensorflow却舀、Pytorch虫几、ONNX等模型的轉(zhuǎn)換(不過(guò)Caffe和Tensorflow的轉(zhuǎn)換器Caffe-Parser和UFF-Parser已經(jīng)有些落后了),也提供了三種轉(zhuǎn)換模型的方式:

  • 使用TF-TRT,將TensorRT集成在TensorFlow中
  • 使用ONNX2TensorRT,即ONNX轉(zhuǎn)換trt的工具
  • 手動(dòng)構(gòu)造模型結(jié)構(gòu)术裸,然后手動(dòng)將權(quán)重信息挪過(guò)去门坷,非常靈活但是時(shí)間成本略高,有大佬已經(jīng)嘗試過(guò)了:tensorrtx

不過(guò)目前TensorRT對(duì)ONNX的支持最好,TensorRT-8最新版ONNX轉(zhuǎn)換器又支持了更多的op操作歼捏。而深度學(xué)習(xí)框架中,TensorRT對(duì)Pytorch的支持更為友好,除了Pytorch->ONNX->TensorRT這條路泉粉,還有:

總而言之歉井,理論上95%的模型都可以轉(zhuǎn)換為T(mén)ensorRT卢佣,條條大路通羅馬嘛冈敛。只不過(guò)有些模型可能轉(zhuǎn)換的難度比較大。如果遇到一個(gè)無(wú)法轉(zhuǎn)換的模型,先不要絕望辈讶,再想想,再想想录语,看看能不能通過(guò)其他方式繞過(guò)去鳄虱。

TensorRT是否支持動(dòng)態(tài)尺寸(dynamic shape)嗎

支持,而且用起來(lái)還很方便潮罪,如果某些OP不支持导而,也可以自己寫(xiě)動(dòng)態(tài)尺度的Plugin实牡。

動(dòng)態(tài)尺度支持NCHW中的N纲堵、H以及W何之,也就是batch溶推、高以及寬蒜危。

對(duì)于動(dòng)態(tài)模型,我們?cè)谵D(zhuǎn)換模型的時(shí)候需要額外指定三個(gè)維度信息即可(最小邀窃、最優(yōu)洁灵、最大)。

舉個(gè)轉(zhuǎn)換動(dòng)態(tài)模型的命令:

./trtexec --explicitBatch --onnx=demo.onnx --minShapes=input:1x1x256x256 --optShapes=input:1x1x2048x2048 --maxShapes=input:1x1x2560x2560 --shapes=input:1x1x2048x2048 --saveEngine=demo.trt --workspace=6000

很簡(jiǎn)單吧~

TensorRT是硬件相關(guān)的

這個(gè)很好明白双抽,因?yàn)椴煌@卡(不同GPU)牍汹,其核心數(shù)量、頻率柬泽、架構(gòu)慎菲、設(shè)計(jì)(還有價(jià)格..)都是不一樣的,TensorRT需要對(duì)特定的硬件進(jìn)行優(yōu)化锨并,不同硬件之間的優(yōu)化是不能共享的露该。

The generated plan files are not portable across platforms or TensorRT versions. Plans are specific to the exact GPU model they were built on (in addition to platforms and the TensorRT version) and must be re-targeted to the specific GPU in case you want to run them on a different GPU

TensorRT是否開(kāi)源

TensorRT是半開(kāi)源的,除了核心部分其余的基本都開(kāi)源了第煮。TensorRT最核心的部分是什么解幼,當(dāng)然是官方展示的一些特性了。如下:

TensorRT的核心部分并沒(méi)有開(kāi)源

以上核心優(yōu)勢(shì)包警,也就是TensorRT內(nèi)部的黑科技撵摆,可以幫助我們優(yōu)化模型,加速模型推理揽趾,這部分當(dāng)然是不開(kāi)源的啦台汇。

而開(kāi)源的部分基本都在這個(gè)倉(cāng)庫(kù)里:

插件相關(guān)、工具相關(guān)、文檔相關(guān)苟呐,里面的資源還是挺豐富的痒芝。

Python中可以使用TensorRT嗎

當(dāng)然是可以的,官方有python的安裝包(下文有說(shuō))牵素,安裝后就可以import tensorrt使用了严衬。

用ldd命令看一下tensorrt.so中都引用了什么。

ldd tensorrt.so
        linux-vdso.so.1 =>  (0x00007ffe477d4000)
        libnvinfer.so.7 => /TensorRT-7.0.0.11/lib/libnvinfer.so.7 (0x00007f2a76f6b000)
        libnvonnxparser.so.7 => /TensorRT-7.0.0.11/lib/libnvonnxparser.so.7 (0x00007f2a76ca5000)
        libnvparsers.so.7 => /TensorRT-7.0.0.11/lib/libnvparsers.so.7 (0x00007f2a76776000)
        libnvinfer_plugin.so.7 => /TensorRT-7.0.0.11/lib/libnvinfer_plugin.so.7 (0x00007f2a758e2000)
        libstdc++.so.6 => /TensorRT-7.0.0.11/lib/libstdc++.so.6 (0x00007f2a75555000)
        libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007f2a7532f000)
        libc.so.6 => /lib64/libc.so.6 (0x00007f2a74f61000)
        libcudnn.so.7 => /usr/local/cuda/lib64/libcudnn.so.7 (0x00007f2a60768000)
        libcublas.so.10.0 => /usr/local/cuda/lib64/libcublas.so.10.0 (0x00007f2a5c1d2000)
        libcudart.so.10.0 => /usr/local/cuda/lib64/libcudart.so.10.0 (0x00007f2a5bf57000)
        libmyelin.so.1 => /TensorRT-7.0.0.11/lib/libmyelin.so.1 (0x00007f2a5b746000)
        libnvrtc.so.10.0 => /usr/local/cuda/lib64/libnvrtc.so.10.0 (0x00007f2a5a12a000)
        libdl.so.2 => /lib64/libdl.so.2 (0x00007f2a59f25000)
        libm.so.6 => /lib64/libm.so.6 (0x00007f2a59c23000)
        /lib64/ld-linux-x86-64.so.2 (0x00007f2a84f5c000)
        libprotobuf.so.16 => /onnx-tensorrt/protobuf-3.6.0/lib/libprotobuf.so.16 (0x00007f2a597ad000)
        libpthread.so.0 => /lib64/libpthread.so.0 (0x00007f2a59591000)
        librt.so.1 => /lib64/librt.so.1 (0x00007f2a59389000)
        libz.so.1 => /lib64/libz.so.1 (0x00007f2a59172000)

TensorRT部署相關(guān)

部署TensorRT的方式笆呆,官方提供了三種:

  • 集成在Tensorflow中使用请琳,比例TF-TRT,這種操作起來(lái)比較便捷赠幕,但是加速效果并不是很好俄精;
  • 在TensorRT Runtime環(huán)境中運(yùn)行模型,就是直接使用TensorRT榕堰;
  • 搭配服務(wù)框架使用竖慧,最配的就是官方的triton-server,完美支持TensorRT逆屡,用在生產(chǎn)環(huán)境杠杠的圾旨!

TensorRT支持哪幾種權(quán)重精度

支持FP32、FP16魏蔗、INT8砍的、TF32等,這幾種類(lèi)型都比較常用莺治。

  • FP32:?jiǎn)尉雀↑c(diǎn)型廓鞠,沒(méi)什么好說(shuō)的,深度學(xué)習(xí)中最常見(jiàn)的數(shù)據(jù)格式谣旁,訓(xùn)練推理都會(huì)用到诫惭;
  • FP16:半精度浮點(diǎn)型,相比FP32占用內(nèi)存減少一半蔓挖,有相應(yīng)的指令值夕土,速度比FP32要快很多;
  • TF32:第三代Tensor Core支持的一種數(shù)據(jù)類(lèi)型瘟判,是一種截短的 Float32 數(shù)據(jù)格式怨绣,將FP32中23個(gè)尾數(shù)位截短為10bits,而指數(shù)位仍為8bits拷获,總長(zhǎng)度為19(=1+8 +10)篮撑。保持了與FP16同樣的精度(尾數(shù)位都是 10 位),同時(shí)還保持了FP32的動(dòng)態(tài)范圍指數(shù)位都是8位)匆瓜;
  • INT8:整型赢笨,相比FP16占用內(nèi)存減小一半未蝌,有相應(yīng)的指令集,模型量化后可以利用INT8進(jìn)行加速茧妒。

簡(jiǎn)單展示下各種精度的區(qū)別:

精度區(qū)別

簡(jiǎn)單舉一個(gè)例子吧萧吠!

說(shuō)了那么多理論知識(shí),不來(lái)個(gè)栗子太說(shuō)不過(guò)去了桐筏。

這個(gè)例子的目的很簡(jiǎn)單纸型,就是簡(jiǎn)單展示一下使用TensorRT的一種場(chǎng)景以及基本流程。假設(shè)老潘有一個(gè)onnx模型想要在3070卡上運(yùn)行梅忌,并且要快狰腌,這時(shí)候就要祭出TensorRT了。

關(guān)于什么是ONNX(ONNX是一個(gè)模型結(jié)構(gòu)格式牧氮,方便不同框架之間的模型轉(zhuǎn)化例如Pytorch->ONNX->TRT)可以看這個(gè)琼腔,這里先不細(xì)說(shuō)了~

老潘手頭沒(méi)有現(xiàn)成訓(xùn)練好的模型,直接從開(kāi)源項(xiàng)目中白嫖一個(gè)吧踱葛。找到之前一個(gè)比較有趣的項(xiàng)目展姐,可以通過(guò)圖片識(shí)別三維人體關(guān)鍵點(diǎn),俗稱(chēng)人體姿態(tài)檢測(cè)剖毯,項(xiàng)目地址在此:

實(shí)現(xiàn)的效果如下,該模型的精度還是可以的教馆,但是畫(huà)面中只能出現(xiàn)一個(gè)目標(biāo)人物逊谋。速度方面的話,主頁(yè)有介紹:

  • GeForce RTX2070 SUPER ? About 30 FPS
  • GeForce GTX1070 ? About 20 FPS
ThreeDPoseTrack

讓我們來(lái)試試TensorRT能夠在3070卡上為其加速多少吧土铺。

看一下模型結(jié)構(gòu)

用到的核心模型在Github主頁(yè)有提供胶滋,即ONNX模型Resnet34_3inputs_448x448_20200609.onnx。作者演示使用的是UnityBarracuda悲敷,其中利用Barracuda去加載onnx模型然后去推理究恤。

我們先用Netron去觀察一下這個(gè)模型結(jié)構(gòu),3個(gè)輸入4個(gè)輸出后德,為什么是3個(gè)輸入呢部宿?其實(shí)這三個(gè)輸入在模型的不同階段,作者訓(xùn)練的時(shí)候的輸入數(shù)據(jù)可能是從視頻中截出來(lái)的連續(xù)3幀的圖像瓢湃,這樣訓(xùn)練可以提升模型的精度(之后模型推理的時(shí)候也需要一點(diǎn)時(shí)間的預(yù)熱理张,畢竟3幀的輸入也是有是時(shí)間連續(xù)性的):

Netron查看模型

使用onnxruntime驗(yàn)證一下模型

一般來(lái)說(shuō),我們?cè)谕ㄟ^(guò)不同框架(Pytorch绵患、TF)轉(zhuǎn)換ONNX模型之后雾叭,需要驗(yàn)證一下ONNX模型的準(zhǔn)確性,否則錯(cuò)誤的onnx模型轉(zhuǎn)成的TensorRT模型也100%是錯(cuò)誤的落蝙。

但顯然上述作者提供的Resnet34_3inputs_448x448_20200609.onnx是驗(yàn)證過(guò)沒(méi)問(wèn)題的织狐,但這邊我們也走一下流程暂幼,以下代碼使用onnxruntime去運(yùn)行:

import onnx
import numpy as np
import onnxruntime as rt
import cv2

model_path = '/home/oldpan/code/models/Resnet34_3inputs_448x448_20200609.onnx'

# 驗(yàn)證模型合法性
onnx_model = onnx.load(model_path)
onnx.checker.check_model(onnx_model)

# 讀入圖像并調(diào)整為輸入維度
image = cv2.imread("data/images/person.png")
image = cv2.resize(image, (448,448))
image = image.transpose(2,0,1)
image = np.array(image)[np.newaxis, :, :, :].astype(np.float32)

# 設(shè)置模型session以及輸入信息
sess = rt.InferenceSession(model_path)
input_name1 = sess.get_inputs()[0].name
input_name2 = sess.get_inputs()[1].name
input_name3 = sess.get_inputs()[2].name

output = sess.run(None, {input_name1: image, input_name2: image, input_name3: image})
print(output)

打印一下結(jié)果看看是什么樣子吧~其實(shí)輸出信息還是很多的,全粘過(guò)來(lái)太多了移迫,畢竟這個(gè)模型的輸出確實(shí)是多...這里只截取了部分內(nèi)容意思一哈:

2021-05-05 10:44:08.696562083 [W:onnxruntime:, graph.cc:3106 CleanUnusedInitializers] Removing initializer 'offset.1.num_batches_tracked'. It is not used by any node and should be removed from the model.
...
[array([[[[ 0.16470502,  0.9578098 , -0.82495296, ..., -0.59656703,
           0.26985374,  0.5808018 ],
         [-0.6096473 ,  0.9780458 , -0.9723106 , ..., -0.90165156,
          -0.8959699 ,  0.91829604],
         [-0.03562748,  0.3730615 , -0.9816262 , ..., -0.9905239 ,
          -0.4543069 ,  0.5840921 ],
         ...,
         ...,
         [0.        , 0.        , 0.        , ..., 0.        ,
          0.        , 0.        ],
         [0.        , 0.        , 0.        , ..., 0.        ,
          0.        , 0.        ],
         [0.        , 0.        , 0.        , ..., 0.        ,
          0.        , 0.        ]]]], dtype=float32)]

看到Removing initializer 'offset.1.num_batches_tracked'. It is not used by any node and should be removed from the model這個(gè)提示我親切地笑了旺嬉,這不就是用Pytorch訓(xùn)練的么,看來(lái)作者這個(gè)模型也是通過(guò)Pytorch訓(xùn)練然后導(dǎo)出來(lái)的起意。

怎么對(duì)比下ONNX和Pytorch輸出結(jié)果是否一致呢鹰服?可以直接看到輸出的數(shù)值是多少,但是這個(gè)模型的輸出還是比較多的揽咕,直接通過(guò)肉眼對(duì)比轉(zhuǎn)換前后的結(jié)果是不理智的悲酷。我們可以通過(guò)代碼簡(jiǎn)單對(duì)比一下:

y = model(x)
y_onnx = model_onnx(x)

# check the output against PyTorch
print(torch.max(torch.abs(y - y_trt)))

ONNX轉(zhuǎn)換為T(mén)ensorRT模型

ONNX模型轉(zhuǎn)換TensorRT模型還是比較容易的,目前TensorRT官方對(duì)ONNX模型的支持最好亲善,而且后續(xù)也會(huì)將精力重點(diǎn)放到ONNX上面(相比ONNX设易,UFF、Caffe這類(lèi)轉(zhuǎn)換工具可能不會(huì)再更新了)蛹头。

目前官方的轉(zhuǎn)換工具TensorRT Backend For ONNX(簡(jiǎn)稱(chēng)ONNX-TensorRT)已經(jīng)比較成熟了顿肺,開(kāi)發(fā)者也在積極開(kāi)發(fā),提issue官方回復(fù)的也比較快渣蜗。我們就用上述工具來(lái)轉(zhuǎn)一下這個(gè)模型屠尊。

我們不需要克隆TensorRT Backend For ONNX,之前下載好的TensorRT包中已經(jīng)有這個(gè)工具的可執(zhí)行文件了耕拷,官方已經(jīng)替我們編譯好了讼昆,只要我們的環(huán)境符合要求,是直接可以用的骚烧。

TensorRT-7.2.3.4/bin中直接使用trtexec這個(gè)工具浸赫,這個(gè)工具可以比較快速地轉(zhuǎn)換ONNX模型以及測(cè)試轉(zhuǎn)換后的trt模型有多快:

我們使用命令轉(zhuǎn)換,可以看到輸出信息:

&&&& RUNNING TensorRT.trtexec # ./trtexec --onnx=Resnet34_3inputs_448x448_20200609.onnx --saveEngine=Resnet34_3inputs_448x448_20200609.trt --workspace=6000
[05/09/2021-17:00:50] [I] === Model Options ===
[05/09/2021-17:00:50] [I] Format: ONNX
[05/09/2021-17:00:50] [I] Model: Resnet34_3inputs_448x448_20200609.onnx
[05/09/2021-17:00:50] [I] Output:
[05/09/2021-17:00:50] [I] === Build Options ===
[05/09/2021-17:00:50] [I] Max batch: explicit
[05/09/2021-17:00:50] [I] Workspace: 6000 MiB
[05/09/2021-17:00:50] [I] minTiming: 1
[05/09/2021-17:00:50] [I] avgTiming: 8
[05/09/2021-17:00:50] [I] Precision: FP32
[05/09/2021-17:00:50] [I] Calibration: 
[05/09/2021-17:00:50] [I] Refit: Disabled
[05/09/2021-17:00:50] [I] Safe mode: Disabled
[05/09/2021-17:00:50] [I] Save engine: Resnet34_3inputs_448x448_20200609.trt
[05/09/2021-17:00:50] [I] Load engine: 
[05/09/2021-17:00:50] [I] Builder Cache: Enabled
[05/09/2021-17:00:50] [I] NVTX verbosity: 0
[05/09/2021-17:00:50] [I] Tactic sources: Using default tactic sources
[05/09/2021-17:00:50] [I] Input(s)s format: fp32:CHW
[05/09/2021-17:00:50] [I] Output(s)s format: fp32:CHW
...
[05/09/2021-17:02:32] [I] Timing trace has 0 queries over 3.16903 s
[05/09/2021-17:02:32] [I] Trace averages of 10 runs:
[05/09/2021-17:02:32] [I] Average on 10 runs - GPU latency: 4.5795 ms  
[05/09/2021-17:02:32] [I] Average on 10 runs - GPU latency: 4.6697 ms 
[05/09/2021-17:02:32] [I] Average on 10 runs - GPU latency: 4.6537 ms 
[05/09/2021-17:02:32] [I] Average on 10 runs - GPU latency: 4.5953 ms 
[05/09/2021-17:02:32] [I] Average on 10 runs - GPU latency: 4.6333 ms 
[05/09/2021-17:02:32] [I] Host Latency
[05/09/2021-17:02:32] [I] min: 4.9716 ms (end to end 108.17 ms)
[05/09/2021-17:02:32] [I] max: 4.4915 ms (end to end 110.732 ms)
[05/09/2021-17:02:32] [I] mean: 4.0049 ms (end to end 109.226 ms)
[05/09/2021-17:02:32] [I] median: 4.9646 ms (end to end 109.241 ms)
[05/09/2021-17:02:32] [I] percentile: 4.4915 ms at 99% (end to end 110.732 ms at 99%)
[05/09/2021-17:02:32] [I] throughput: 0 qps
[05/09/2021-17:02:32] [I] walltime: 3.16903 s
[05/09/2021-17:02:32] [I] Enqueue Time
[05/09/2021-17:02:32] [I] min: 0.776001 ms
[05/09/2021-17:02:32] [I] max: 1.37109 ms
[05/09/2021-17:02:32] [I] median: 0.811768 ms
[05/09/2021-17:02:32] [I] GPU Compute
[05/09/2021-17:02:32] [I] min: 4.5983 ms
[05/09/2021-17:02:32] [I] max: 4.1133 ms
[05/09/2021-17:02:32] [I] mean: 4.6307 ms
[05/09/2021-17:02:32] [I] median: 4.5915 ms
[05/09/2021-17:02:32] [I] percentile: 4.1133 ms at 99%

其中FP32推理速度是4-5ms左右赃绊,而FP16只需要1.6ms既峡。

PS:關(guān)于ONNX-TensorRT這個(gè)工具,本身是由C++寫(xiě)的碧查,整體結(jié)構(gòu)設(shè)計(jì)的比較緊湊运敢,值得一讀,之后老潘會(huì)講述ONNX-TensorRT這個(gè)工具的編譯和使用方法忠售。

運(yùn)行TensorRT模型

這里我們使用TensorRT的Python端加載轉(zhuǎn)換好的resnet34_3dpose.trt模型者冤。使用Python端時(shí)首先需要安裝TensorRT-tar包下的pyhton目錄下的tensorrt-7.2.3.4-cp37-none-linux_x86_64.whl安裝包,目前7.0支持最新的python版本為3.8档痪,而TensorRT-8-EA則開(kāi)始支持python-3.9了涉枫。

安裝Python-TensorRT后,首先import tensorrt as trt腐螟。

然后加載Trt模型:

logger = trt.Logger(trt.Logger.INFO)
  with open("resnet34_3dpose.trt", "rb") as f, trt.Runtime(logger) as runtime:
    engine=runtime.deserialize_cuda_engine(f.read())

加載好之后愿汰,我們打印下這個(gè)模型的輸入輸出信息困后,觀察是否與ONNX模型一致:

for idx in range(engine.num_bindings):
    is_input = engine.binding_is_input(idx)
    name = engine.get_binding_name(idx)
    op_type = engine.get_binding_dtype(idx)
    model_all_names.append(name)
    shape = engine.get_binding_shape(idx)

    print('input id:',idx,'   is input: ', is_input,'  binding name:', name, '  shape:', shape, 'type: ', op_type)

可以看到:

engine bindings message: 
input id: 0    is input:  True   binding name: input.1   shape: (1, 3, 448, 448) type:  DataType.FLOAT
input id: 1    is input:  True   binding name: input.4   shape: (1, 3, 448, 448) type:  DataType.FLOAT
input id: 2    is input:  True   binding name: input.7   shape: (1, 3, 448, 448) type:  DataType.FLOAT
input id: 3    is input:  False   binding name: 499   shape: (1, 24, 28, 28) type:  DataType.FLOAT
input id: 4    is input:  False   binding name: 504   shape: (1, 48, 28, 28) type:  DataType.FLOAT
input id: 5    is input:  False   binding name: 516   shape: (1, 672, 28, 28) type:  DataType.FLOAT
input id: 6    is input:  False   binding name: 530   shape: (1, 2016, 28, 28) type:  DataType.FLOAT

3個(gè)輸入4個(gè)輸出,完全一致沒(méi)有問(wèn)題衬廷!

然后載入圖像摇予,運(yùn)行模型:

image = cv2.imread(image_path)
image = cv2.resize(image, (200,64))
image = image.transpose(2,0,1)
img_input = image.astype(np.float32)
img_input = torch.from_numpy(img_input)
img_input = img_input.unsqueeze(0)
img_input = img_input.to(device)
# 運(yùn)行模型
result_trt = trt_model(img_input)

咦?這么簡(jiǎn)單么吗跋,trt的engine在哪兒侧戴?

當(dāng)然不是,其實(shí)這個(gè)trt_model是一個(gè)類(lèi)跌宛,我們這樣使用它:

# engine即上述加載的engine
trt_model = TRTModule(engine, ["input.1", "input.4", "input.7"])

而這個(gè)TRTModule是什么呢酗宋,為了方便地加載TRT模型以及創(chuàng)建runtime,我們借鑒torch2trt這個(gè)庫(kù)中的一個(gè)實(shí)現(xiàn)類(lèi)疆拘,比較好地結(jié)合了Pytorch與TensorRT蜕猫,具體實(shí)現(xiàn)如下:

class TRTModule(torch.nn.Module):
    def __init__(self, engine=None, input_names=None, output_names=None):
        super(TRTModule, self).__init__()
        self.engine = engine
        if self.engine is not None:
            # engine創(chuàng)建執(zhí)行context
            self.context = self.engine.create_execution_context()

        self.input_names = input_names
        self.output_names = output_names

    def forward(self, *inputs):
        batch_size = inputs[0].shape[0]
        bindings = [None] * (len(self.input_names) + len(self.output_names))

        for i, input_name in enumerate(self.input_names):
            idx = self.engine.get_binding_index(input_name)
            # 設(shè)定shape 
            self.context.set_binding_shape(idx, tuple(inputs[i].shape))
            bindings[idx] = inputs[i].contiguous().data_ptr()

        # create output tensors
        outputs = [None] * len(self.output_names)
        for i, output_name in enumerate(self.output_names):
            idx = self.engine.get_binding_index(output_name)
            dtype = torch_dtype_from_trt(self.engine.get_binding_dtype(idx))
            shape = tuple(self.context.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] = output
            bindings[idx] = output.data_ptr()

        self.context.execute_async_v2(bindings,
                                      torch.cuda.current_stream().cuda_stream)

        outputs = tuple(outputs)
        if len(outputs) == 1:
            outputs = outputs[0]

        return outputs

我們重點(diǎn)看__init__()forward這兩個(gè)成員方法,即engine創(chuàng)建context哎迄,然后確定輸入輸出回右,執(zhí)行execute_async_v2即可獲取結(jié)果。

就這樣漱挚,使用TensorRT調(diào)用已經(jīng)序列化好的trt模型就成功啦翔烁。

利用Polygraphy查看ONNX與TRT模型的輸出差異

Polygraphy是TensorRT官方提供的一系列小工具合集,通過(guò)這個(gè)工具我們看一下這個(gè)Resnet34_3inputs_448x448_20200609.onnx模型在轉(zhuǎn)換為trt之后是否會(huì)有精度折損:

首先看一下FP32精度:

使用polygraphy查看精度對(duì)比-FP32

再看下FP16精度:

fp16的精度對(duì)比失敗

這里的絕對(duì)誤差和相對(duì)誤差容忍度設(shè)置的均為1e-3旨涝,精確到小數(shù)點(diǎn)后3位蹬屹,可以看到上述onnx模型在轉(zhuǎn)化為FP32的trt是沒(méi)有大問(wèn)題的,而FP16則有比較多的精度折損颊糜。至于會(huì)不會(huì)嚴(yán)重地影響結(jié)果需要我們通過(guò)具體或者一批case分析。限于篇幅放到后續(xù)說(shuō)秃踩。

例子就這樣~

TensorRT的缺點(diǎn)

TensorRT不是沒(méi)有“缺點(diǎn)”的衬鱼,有一些小小的缺點(diǎn)需要吐槽一下:

  • 經(jīng)過(guò)infer優(yōu)化后的模型與特定GPU綁定,例如在1080TI上生成的模型在2080TI上無(wú)法使用憔杨;
  • 高版本的TensorRT依賴(lài)于高版本的CUDA版本鸟赫,而高版本的CUDA版本依賴(lài)于高版本的驅(qū)動(dòng),如果想要使用新版本的TensorRT消别,更換環(huán)境是不可避免的抛蚤;
  • TensorRT盡管好用,但推理優(yōu)化infer還是閉源的寻狂,像深度學(xué)習(xí)煉丹一樣岁经,也像個(gè)黑盒子,使用起來(lái)會(huì)有些畏手畏腳蛇券,不能夠完全掌控缀壤。所幸TensorRT提供了較為多的工具幫助我們調(diào)試樊拓。

正所謂愛(ài)之深恨之切,老潘也知道塘慕,這些”缺點(diǎn)“也是沒(méi)有辦法的缺點(diǎn)筋夏,既然無(wú)法避免,就輕輕吐槽一下吧图呢。

TensorRT配套周邊

TensorRT畢竟發(fā)展這么多年了条篷,官方也深知大家使用TensorRT的一些痛點(diǎn),所以開(kāi)發(fā)了一些比較實(shí)用的小工具來(lái)供大家使用蛤织,工具目前在TensorRT的開(kāi)源主頁(yè)赴叹,也就是說(shuō)這些工具也是開(kāi)源的:

TensorRT自帶的小工具

這三個(gè)工具的基本功能大概介紹下:

  • ONNX GraphSurgeon 可以修改我們導(dǎo)出的ONNX模型,增加或者剪掉某些節(jié)點(diǎn)瞳筏,修改名字或者維度等等
  • Polygraphy 各種小工具的集合稚瘾,例如比較ONNX和trt模型的精度,觀察trt模型每層的輸出等等姚炕,主要用來(lái)debug一些模型的信息摊欠,還是比較有用的
  • PyTorch-Quantization 可以在Pytorch訓(xùn)練或者推理的時(shí)候加入模擬量化操作,從而提升量化模型的精度和速度柱宦,并且支持量化訓(xùn)練后的模型導(dǎo)出ONNX和TRT

老潘用過(guò)ONNX GraphSurgeon以及Polygraphy些椒,兩者都是在實(shí)際部署轉(zhuǎn)換中比較實(shí)用的工具,也確實(shí)解決了一些問(wèn)題掸刊。唯一不足的是這些工具的教程都不是很詳細(xì)免糕,較難上手,之后老潘也會(huì)詳細(xì)介紹一哈這些工具的使用方法忧侧。

參考鏈接

后記

寫(xiě)不動(dòng)了先這樣吧...關(guān)于TensorRT想寫(xiě)的還有很多石窑,不想錯(cuò)過(guò)可以關(guān)注我哦,希望這篇文章能夠?qū)W(xué)習(xí)TensorRT的各位有些幫助~

個(gè)人來(lái)講蚓炬,TensorRT最好的學(xué)習(xí)方式還是實(shí)戰(zhàn)松逊,只有踩過(guò)一些坑,改過(guò)一些代碼肯夏,跑通過(guò)一些程序经宏,才能更好地學(xué)習(xí)TensorRT。而所謂的那么些文檔驯击,也無(wú)非是幫助我們少踩些坑罷了烁兰。無(wú)論如何,大部分坑該踩的還是踩的徊都,不要等準(zhǔn)備好再上沪斟,趕緊的,代碼跑起來(lái)吧暇矫!

嘿嘿嘿

有疑問(wèn)或者相關(guān)問(wèn)題歡迎拍磚留言~

撩我吧

  • 如果你與我志同道合于此币喧,老潘很愿意與你交流轨域;
  • 如果你喜歡老潘的內(nèi)容,歡迎關(guān)注和支持杀餐。
  • 如果你喜歡我的文章干发,希望點(diǎn)贊?? 收藏 ?? 評(píng)論 ?? 三連一下~

想知道老潘是如何學(xué)習(xí)踩坑的,想與我交流問(wèn)題~請(qǐng)關(guān)注公眾號(hào)「oldpan博客」史翘。
老潘也會(huì)整理一些自己的私藏枉长,希望能幫助到大家,點(diǎn)擊神秘傳送門(mén)獲取琼讽。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末必峰,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子钻蹬,更是在濱河造成了極大的恐慌吼蚁,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件问欠,死亡現(xiàn)場(chǎng)離奇詭異郑什,居然都是意外死亡垮斯,警方通過(guò)查閱死者的電腦和手機(jī)伐厌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)定嗓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人注整,你說(shuō)我怎么就攤上這事能曾。” “怎么了肿轨?”我有些...
    開(kāi)封第一講書(shū)人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵寿冕,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我椒袍,道長(zhǎng)驼唱,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任槐沼,我火速辦了婚禮曙蒸,結(jié)果婚禮上捌治,老公的妹妹穿的比我還像新娘岗钩。我一直安慰自己,他們只是感情好肖油,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布兼吓。 她就那樣靜靜地躺著,像睡著了一般森枪。 火紅的嫁衣襯著肌膚如雪视搏。 梳的紋絲不亂的頭發(fā)上审孽,一...
    開(kāi)封第一講書(shū)人閱讀 51,679評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音浑娜,去河邊找鬼佑力。 笑死,一個(gè)胖子當(dāng)著我的面吹牛筋遭,可吹牛的內(nèi)容都是我干的打颤。 我是一名探鬼主播,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼漓滔,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼编饺!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起响驴,我...
    開(kāi)封第一講書(shū)人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤透且,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后豁鲤,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體秽誊,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年畅形,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了养距。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡日熬,死狀恐怖棍厌,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情竖席,我是刑警寧澤耘纱,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站毕荐,受9級(jí)特大地震影響束析,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜憎亚,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一员寇、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧第美,春花似錦蝶锋、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至,卻和暖如春躯舔,著一層夾襖步出監(jiān)牢的瞬間驴剔,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來(lái)泰國(guó)打工粥庄, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留丧失,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓惜互,卻偏偏與公主長(zhǎng)得像利花,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子载佳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容