Knowhow/TensorRT

[TensorRT 튜토리얼] 4. How TensorRT works

침닦는수건 2024. 1. 23. 11:41
반응형

구현된 방식

TensorRT API는 class-based다. "class를 wrapping하는 class" 이런 식으로 전부 class로 구현되어 있다.

 

최상위 class (e.g. builder, runtime)가 존재하고 하위 class들이 member를 구성하고 있으며, 최상위 객체가 호출하면서 사용하는 방식이다. 그러므로 실제로 코드는 최상위 class (builder, runtime)에 단 2가지에 의해 동작하는 모양이다.

 

따라서 최상위 class를 먼저 생성하는 것으로 시작하고 최상위 class를 지우면서 종료된다.

 

구체적으로 최상위 클래스는 conversion 과정에서는 Builder, deployment 과정에서는 Runtime이 존재한다.


Logging and error handling

Top level interface(class)인 Builder와 Runtime 객체를 생성할 때 필수적으로 Logger 객체를 같이 넣어주도록 되어있다. 변conversion/deplyment 과정에서 발생하는 오류, 경고, 안내, 결과 등 log를 기록해두기 위함이다. (verbosity 조절 가능)  

 

Logger가 builder, runtime에 공통으로 들어가는 만큼 생성되고 나서 중복되지 않도록, thread-safe하게 생성해서 사용해야 한다. (한 번 생성하고 나서 중간에 없애거나 새로 생성하지 말고 계속 공유해서 쓰라는 말.)

 

Builder, runtime 내부에 들어가있기 때문에 직접 호출하며 사용할 일은 없다. Builder, runtime이 내부적으로 알아서 사용한다. 예를 들어 context.enqueueV3 호출되면 runtime이 알아서 logger 에 기록하기 시작한다.

 

error만 따로 다룰 수 있기도 하다. 기본적으론 logger가 다 기록하기 때문에 굳이 따로 둘 필요는 없으나, 나중에 디버깅 편의 상 error만 따로 분리할 수 있다.

 

ErrorRecoder 라는 class를 사용하면 된다. 이 객체의 경우, 하나만 존재할 필요는 없고 engine용, context용 등 다수 존재해도된다. 명시적으로 생성하지 않으면 전부 logger가 담당한다. 


Build phase(conversion)

이 과정은 model definition을 전달하고 target GPU에 맞게 optimize하는 과정이다. 

 

여기서 사용될 highest level interface, 즉 최상위 class는 Builder 임. builder가 model 최적화하고 engine 뱉어주는 형태다.


 

build 입력은 다음과 같다.

 

1) model definition

NetworkDefinition class를 만들고 내용을 채워넣으면 된다. 가장 유명한 방식은 그냥 onnx format으로 변환한 것을 넣어주는 방식으로 ONNX paraser가 NetworkDefinition으로 parsing해주는 방식이다.

 

이 방식이 아니라면 TensorRT Layer(+하위 클래스)들을 이용해 직접 NetworkDefinition을 구현해주어야 된다. pytorch에서 nn.Module 상속받고 직접 모델 디자인하는 것과 같은 형태로, 숙련도가 필요하여 권장되지 않는 방식이다. 

 

동시에 model definition에는 input/output tensor에 대한 정의가 필수적이다. 어떤 tensor를 입력으로 쓸건지 출력으로 쓸건지 미리 정해서 builder가 최적화 과정에서 제거하거나 무시하지 않도록 정해줘야 되기 때문이다. 혹시라도 그냥 중요하지 않은 tensor으로 보아 최적화 과정에서 제외할 수도 있다. 

 

input, output tensor에 이름을 부여하고 binding하는 것까지 model definition에 포함됨. (이는 뒤 runtime에서 이름보고 찾아서 쓸 수 있게 만든다.)

 

Builder를 동작시키면, dead computation elimination, constant folding, operation 재배치 + 조합 마치 컴파일러가 하는 것들을 적용하면서 GPU버전으로 최적화 한다. 이 때 GPU는 달려있는 GPU다. 당연하게도 현재 인식되는 장치만 고려할 수 있기 때문이다.

  

 

2) builder configuration

BuilderConfig class를 만들고 내용을 채워넣으면 된다. 어떻게 TensorRT가 모델을 최적화해야되는지 그 디테일을 기록하는 것으로, precision 어떻게 낮출 것인지 memory vs runtime execution speed 조절, CUDA kernel 지정 등 최적화 디테일을 결정하는 내용이다.

 

temporary memory 사용량을 제한하거나, workspace size가 기본적으로 device global memory 전체로 잡혀있는데 이런 메모리 제한을 푸는 것과 같이 하드웨어 관련된 설정도 이 과정에서 가능하다. 

 

precision 낮추는 작업을 일부만 / 전체만 할건지, 16bit float/ 8bit int으로 할 것인지 고를 수 있고 심지어는 여러 data type 동시 테스트해보고 최적이 어떤 type인지 확인하여 고를 수도 있다. 


 

builder의 출력은 Plan 이라고 불리는 serialized engine이다. 저장 용량을 줄이기 위해서 serialized 형태로 나오고 추후 runtime 때 deserialize하는 과정이 있다. 결과물은 TensorRT의 버전, GPU 모델에 당연히 맞춰진 engine이다.

 

Memory details

이 단계에서 모델 weight는 최소 2개 이상으로 복사돼서 호스트 메모리에 저장된다. (원본 모델, engine, +@)

추가적으로 conv+batch norm 같이 layer combination 가능한 조합들은 합치는 과정에서 추가 temporary weight tensor를 만들어서 사용하기도 한다.

 

Note

NetworkDefinition을 정의할 때 shallow-copy임 (그냥 주소 복사). loading된 원본 모델 weight 주소만 갖고 작업하기 때문에 building하는 과정에서 원본 모델 delete하거나 지우면 안된다.


Runtime phase(deployment)

이 과정에서 사용될 highest level interface, builder랑 동급인 최상위 class, Runtime 이 있다. Runtime은 plan을 실제 실행할 수 있는 형태로 변환해주는 역할로 총 3가지 기능을 담당한다.

  1. deserialize plan
  2. create execution context  
  3. inference

저장용량을 줄이기 위해 serialized된 engine을 다시 원상복구 시키고 이를 실행시키기 위해 필요한 context를 생성하고, 실제 inference한다.

 

 

Runtime이 plan을 입력으로 받아 Engine(deserialized plan)을 생성하는 것이 시작이다. Engine이 최적화된 최종형태 모델을 loading한 상태로 engine에 input output tensor랑 동시에 data dimension, format 등을 입력하면 inference가 동작한다.

 

구체적으로 inference는 ExecutionContext 에 의해 호출된다. Engine에서 생성하는 객체인데 이름이 왜이런지 모르겠지만 inference를 호출해주는 역할이다. Engine 동작시키라고 명령 내리는 사람 같은 역할이다. inference 단계에서는 input buffers를 생성하고 ExecutionContext 내부 함수 enqueueV3를 계속 호출하면서 데이터를 흘려보내는 역할을 한다.

 

같은 engine을 다른 방식으로 동작시킬 수도 있기 때문에 multi-context가 존재할 수 있다. 

 

 

Memory details

host memory 조금 쓰고 device memory 많이 쓴다. engine이 이 단계에서 deserialization 되면서 device memory에 올라간다.

 

Note

context 이용해서 inference 시작하기 전에 input output buffers를 미리 선언해두는게 중요하다. CPU에 올려둘건지, GPU에 올려둘건지 결정해야 하며, 이걸 정하기 어려운 모델인 경우(CPU, GPU 둘 다 쓰는 경우) engine한테 어떤 momery space 쓸건지 알려줘야 된다.

 

input/ouput buffer까지 준비되면 enqueueV3를 호출하면서 inference가 진행된다. enqueue라는 이름이 inference인 이유는, CUDA stream에 required kernel을 순서대로 쌓기 때문이다. enqueue 호출되면 device가 어떻게 동작하면 될지 동작 명령 (control)이 return 값으로 돌아오게 된다.

 

이게 모델에 따라 (아까 CPU, GPU 다쓰는 애들) returned control이 불규칙하게 들어올 수 있는데 이걸 전부 다 그랩할 때까지 기다리도록 하기 위해서 cudaStreamSynchronize를 쓴다고 한다.

반응형