TensorRT5.1.5.0 實踐 Doinference過程的探究(python)

.
  找了個時間研究一下下python下,到底是怎麼實現doinference的。
  第一次接觸tensorRT的時候,主頁上就寫了tensorRT是通過輸入某種框架產生的模型及其weights,就可以進行  優化生成一個engine,用這個engine多推斷,推斷會加速。
  具體的來說,就是model->engine->結果。這部分想主要學習一下構建及使用engine的具體代碼,也就是很多demo中用到的doinference過程,我是以TensorRT的onnx_yolo-v3的demo中的代碼來學習這個過程的。
  嚴格來說,這個demo中,進行推斷的過程如下:

    with get_engine(onnx_file_path, engine_file_path) as engine, engine.create_execution_context() as context:
        inputs, outputs, bindings, stream = common.allocate_buffers(engine)
        print('Running inference on image {}...'.format(input_image_path))
        inputs[0].host = image
        trt_outputs = common.do_inference(context, bindings=bindings, inputs=inputs, outputs=outputs, stream=stream)
    trt_outputs = [output.reshape(shape) for output, shape in zip(trt_outputs, output_shapes)]

get_engine

def get_engine(onnx_file_path, engine_file_path=""):
    def build_engine():
        with trt.Builder(TRT_LOGGER) as builder, builder.create_network() as network, trt.OnnxParser(network, TRT_LOGGER) as parser:
            builder.max_workspace_size = 1 << 30 # 1GB
            builder.max_batch_size = 1
            # Parse model file
            if not os.path.exists(onnx_file_path):
                print('ONNX file {} not found, please run yolov3_to_onnx.py first to generate it.'.format(onnx_file_path))
                exit(0)
            print('Loading ONNX file from path {}...'.format(onnx_file_path))
            with open(onnx_file_path, 'rb') as model:
                print('Beginning ONNX file parsing')
                parser.parse(model.read())
            print('Completed parsing of ONNX file')
            print('Building an engine from file {}; this may take a while...'.format(onnx_file_path))
            engine = builder.build_cuda_engine(network)
            print("Completed creating Engine")
            with open(engine_file_path, "wb") as f:
                f.write(engine.serialize())
            return engine
            
    if os.path.exists(engine_file_path):
        # If a serialized engine exists, use it instead of building an engine.
        print("Reading engine from file {}".format(engine_file_path))
        with open(engine_file_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
            return runtime.deserialize_cuda_engine(f.read())
    else:
        return build_engine()

get_engine()方法判別engine是否存在,存在的話直接用trt.Runtime()類的runtime對象下的deserialize_cuda_engine方法讀取,如下所示:

with open(engine_file_path, "rb") as f, trt.Runtime(TRT_LOGGER) as runtime:
     return runtime.deserialize_cuda_engine(f.read())

如果不存在,調用build_engine方法先後創建builder, network, parser;然後,運用parser.parse()方法解析model,builder.build_cuda_engine方法根據網絡創建engine;最後,利用engine.serialize()方法把創建好的engine並行化(保存爲二進制流),存在”engine_file_path“這個文件下。

engine.create_execution_context()

這是個封裝好的函數,是閉源的,c++裏面的聲明是這樣的。

    //!
    //! \brief Create an execution context.
    //!
    //! \see IExecutionContext.
    //!
    virtual IExecutionContext* createExecutionContext() = 0;

就是創建了一個在runtime過程中所必需的context。(最後也要destroy)

common.allocate_buffers()

nputs, outputs, bindings, stream = common.allocate_buffers(engine)

allocate_buffers方法爲這個創造出來的engine分配空間。

def allocate_buffers(engine):
    inputs = []
    outputs = []
    bindings = []
    stream = cuda.Stream()
    for binding in engine:
        size = trt.volume(engine.get_binding_shape(binding)) * engine.max_batch_size
        dtype = trt.nptype(engine.get_binding_dtype(binding))
        host_mem = cuda.pagelocked_empty(size, dtype)
        device_mem = cuda.mem_alloc(host_mem.nbytes)
        bindings.append(int(device_mem))
        # Append to the appropriate list.
        if engine.binding_is_input(binding):
            inputs.append(HostDeviceMem(host_mem, device_mem))
        else:
            outputs.append(HostDeviceMem(host_mem, device_mem))
    return inputs, outputs, bindings, stream

for循環之外

在這裏面創建了一個cuda流,然後就是對input/output的分析,以及空間分配了。

for循環之內

---- binding定義爲list,在engine中4次循環取到的分別是unicode類型的:u’000_net’, u’082_convolutional’, u’094_convolutional’, u’106_convolutional’ 。而這些值代表的恰好就是一個input和三個output。

1. size = trt.volume(engine.get_binding_shape(binding)) * engine.max_batch_size
trt.volume方法是計算迭代空間的方法:

def volume(iterable):
    vol = 1
    for elem in iterable:
        vol *= elem
    return vol if iterable else 0

而debug顯示,此時的engine.get_binding_shape(binding)的值爲Dims類型的(608,608,3),顯而易見,trt.volume就是把圖片鋪平了,計算佔位;最後再乘以engine.max_batch_size,計算HCHW的總的內存佔用size。(這裏還沒沒有按照精度計算內存空間,相當於只計算了像素個數)

2. dtype = trt.nptype(engine.get_binding_dtype(binding))
trt.nptype是返回type的:

def nptype(trt_type):
    import numpy as np
    if trt_type == float32:
        return np.float32
    elif trt_type == float16:
        return np.float16
    elif trt_type == int8:
        return np.int8
    elif trt_type == int32:
        return np.int32
    raise TypeError("Could not resolve TensorRT datatype to an equivalent numpy datatype.")

這裏返回了type,因爲不同精度佔的內存不同,所以需要知道type。

3. host_mem = cuda.pagelocked_empty(size, dtype)
根據size和type創建page-locked的內存緩衝區(sizetype所佔byte數)
4. device_mem = cuda.mem_alloc(host_mem.nbytes)
創建設備存儲區。其中host_mem.nbytes就是計算出來的int值,比如在我跑tensorRT的yolov3中,推斷過程中batchsize等於1,輸入爲1
6086083,對應的float類型爲32位,佔4個byte,所以最後佔用的byte數爲1608608*4= 4435968個byte。
而我debug出來的也正是如此。
在這裏插入圖片描述

最後,list類型的binding存儲了,每個input/output的內存佔有量。

5.    if engine.binding_is_input(binding):
            inputs.append(HostDeviceMem(host_mem, device_mem))
      else:
           outputs.append(HostDeviceMem(host_mem, device_mem))

如果是input,就在inputs的list中append出一個對應的HostDeviceMem對象;如果是output,就在outputs類裏面append一個對應的HostDeviceMem對象。HostDeviceMem對象的結構如下所示:

class HostDeviceMem(object):
    def __init__(self, host_mem, device_mem):
        self.host = host_mem
        self.device = device_mem

    def __str__(self):
        return "Host:\n" + str(self.host) + "\nDevice:\n" + str(self.device)

    def __repr__(self):
        return self.__str__()

所以,總結起來,allocate_buffers方法就是爲輸入和輸出開闢了內存。每個input/ouput都生成兩個ndarray類型的host_mem,device_mem,封裝在HostDeviceMem類中,用來存inference的結果。

common.doinference()

除了設置了一下inputs[0]中的host(也就是上面求出的input的host_mem)的值,即給input賦值

inputs[0].host=image

就是做doinference了。

trt_outputs = common.do_inference(context, bindings=bindings, inputs=inputs, outputs=outputs, stream=stream)

doinference方法只有五行代碼:

def do_inference(context, bindings, inputs, outputs, stream, batch_size=1):
    # Transfer input data to the GPU.
    [cuda.memcpy_htod_async(inp.device, inp.host, stream) for inp in inputs]
    # Run inference.
    context.execute_async(batch_size=batch_size, bindings=bindings, stream_handle=stream.handle)
    # Transfer predictions back from the GPU.
    [cuda.memcpy_dtoh_async(out.host, out.device, stream) for out in outputs]
    # Synchronize the stream
    stream.synchronize()
    # Return only the host outputs.
    return [out.host for out in outputs]

輸入的參數

context

context是通過engine.create_execution_context()生成的,create_execution_context是寫在ICudaEngine.py的一個閉源方法,這個方法是創建立一個IExecutionContext類型的對象。
在python中,這個類是寫在IExecutionContext中,

class IExecutionContext(__pybind11_builtins.pybind11_object):
	def execute(self, batch_size, bindings, p_int=None): # real signature unknown; restored from __doc__
	def execute_async(self, batch_size=1, bindings, p_int=None, *args, **kwargs): # real signature unknown; NOTE: unreliably restored from __doc__
	def __del__(self): # real signature unknown; restored from __doc__
        """ __del__(self: tensorrt.tensorrt.IExecutionContext) -> None """
        pass
    def __enter__(self, *args, **kwargs): # real signature unknown
        pass
    def __exit__(self, *args, **kwargs): # real signature unknown
        pass
    def __init__(self, *args, **kwargs): # real signature unknown
        pass
    debug_sync = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default
    device_memory = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default
    engine = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default
    name = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default
    profiler = property(lambda self: object(), lambda self, v: None, lambda self: None)  # default

又去 c++ 的NvInfer.h中看了IExecutionContext類的一些方法。

class IExecutionContext
{
public:
	# 在一個batch上同步的執行inference
	virtual bool execute(int batchSize,void** bindings)=0;
	# 在一個batch上異步的執行inference
	virtual bool enqueue(int batchSize, void** bindings, cudaStream_t stream, cudaEvent_t* inputConsumed)=0;
	# 設置Debug sync flag
	virtual void setDebugSync(bool sync)=0;
	# 得到 Debug sync flag
	virtual bool getDebugSync()=0;
	# 設置 profiler
	virtual void setProfiler(IProfiler*)=0;
	# 得到 profiler
	virtual const ICudaEngine& getEngine() const=0;
	# 得到鏈接的engine
	virtual const getEngine() const=0;
	virtual void destroy()=0;
protected:
	virtual ~IExecutionContext(){}
public:
	# 設置execution context的名字
	virtual void setName(const char* name)=0;
	# 返回該名字的execution context
	virtual const char* getName() const =0;
	# 設置需要的device memory
	virtual void setDeviceMemory(void* memory)=0;
}

所以,大同小異,context是用來執行推斷的對象,在C++中,是利用execute或者enqueue方法進行推斷執行,但是在python中則是利用execute和execute_async兩個函方法。

bindings

bindings中存的是每個input/output所佔byte數的int值,上面已經有介紹

inputs,outputs

inputs,outputs是有一個個HostDeviceMem類型組成的list,比如inputs[0]就在之前的步驟被賦值爲預處理後的image,而outputs在沒有執行推斷之前,則是值爲0,outputs大小的三個HostDeviceMem對象。

stream

stream爲在allocate_buffers中由cuda.Stream()生成的stream,來自於Stream.py,但是這個不是TensorRT的東西,而來自於pycuda,是cuda使用過程不可缺少的一員。

執行的操作

def do_inference(context, bindings, inputs, outputs, stream, batch_size=1):
    # Transfer input data to the GPU.
    [cuda.memcpy_htod_async(inp.device, inp.host, stream) for inp in inputs]
    # Run inference.
    context.execute_async(batch_size=batch_size, bindings=bindings, stream_handle=stream.handle)
    # Transfer predictions back from the GPU.
    [cuda.memcpy_dtoh_async(out.host, out.device, stream) for out in outputs]
    # Synchronize the stream
    stream.synchronize()
    # Return only the host outputs.
    return [out.host for out in outputs]

cuda.memcpy_htod_async()

python中,沒有給確切的註釋,但是在NVIDIA CUDA Library Documentation中找到了類似的方法:cuMemcpyHtoD方法。
這個方法 從主機內存複製數據到設備內存。 dstDevice和srcHost分別是目標和源的基地址。 ByteCount指定要複製的字節數。
所以,cuda.memcpy_htod_async方法是把input中的數據從主機內存複製到設備內存(遞給GPU),而inputs中的元素恰好是函數可以接受的HostDeviceMem類型。

context.execute_async

這個在上面介紹context中已經提到,利用GPU執行推斷的步驟,這裏應該是異步的。

cuda.memcpy_dtoh_async

推斷,和上面的memcpy_htod_async相同,也有個類似的方法cuMemcpyDtoH,即把計算完的數據從device(GPU)拷回host memory中。

stream.synchronize()

這是在Context.py中的方法,和同步有關,但是目前的知識,還不太會解釋。

函數的最後,把存在HostDeviceMem類型的outpus中的host中的數據,取出來,放在一個list中返回。

當然,這是python中doinference的一種實現方法,C++中比較亂,還需要繼續學習。

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