ONNX를 거쳐 TRT engine으로 변환이 완료된 모델을 실제로 사용하는 방법이다. .trt 확장자로 저장된 파일을 열어 inferece하는 방법으로 각각 TensorRT python API, c++ API를 통해 python/c++ 코드 내에서 동작시킬 수 있다.
Deplying to python API
document 보면 onnx_helper 내 ONNXClassifierWrapper로 예제가 적혀있는데 이건 오로지 예제를 위한 것이다. imagenet classification에 맞추어진 것이라고 보면 됨. Pytorch to TensorRT through ONNX 튜토리얼 글을 참고하는 것이 옳다.
import time
import torch
import numpy as np
import tensorrt as trt
import pycuda.driver as cuda
import pycuda.autoinit
import torchvision.models as models
### preparations
batch_size = 8
dummy_img = np.ones([batch_size, 224, 224, 3])
dummy_batch = torch.randn(batch_size, 3, 224, 224)
model = models.resnet50(pretrained=True, progress=False).eval()
필요한 입력과 모델 준비.
### engine loading
f = open("./resnet_engine.trt", "rb")
runtime = trt.Runtime(trt.Logger(trt.Logger.WARNING))
engine = runtime.deserialize_cuda_engine(f.read())
context = engine.create_execution_context() # 여기까지만 적으면 터짐...
# need to set input and output precisions to FP16 to fully enable it
output = np.empty([batch_size, 1000], dtype=np.float16)
# allocate device memory
d_input = cuda.mem_alloc(1 * dummy_img.nbytes)
d_output = cuda.mem_alloc(1 * output.nbytes) # 여기까지 해야 안터짐.
주의할 점이 context까지만 선언하면 코드가 segmentation fault가 떠버린다.
[01/16/2024-11:14:49] [TRT] [E] 1: [defaultAllocator.cpp::deallocate::61] Error Code 1: Cuda Runtime (invalid argument)
[01/16/2024-11:14:49] [TRT] [E] 1: [cudaResources.cpp::~ScopedCudaStream::47] Error Code 1: Cuda Runtime (invalid device context)
[01/16/2024-11:14:49] [TRT] [E] 1: [cudaResources.cpp::~ScopedCudaEvent::24] Error Code 1: Cuda Runtime (context is destroyed)
위와 같은 알 수 없는 오류를 볼 수 있음. 이유는 tensorrt engine은 모델 자체만 포함하는 것이 아니라 입력/출력 데이터가 저장될 메모리도 포함한다. 따라서 모델을 실제 사용할 디바이스에 올린 뒤 입력/출력에 사용할 메모리를 할당해두는 작업을 기다리는데, 메모리 할당 작업 없이 코드가 종료되니 오류가 난다고 볼 수 있다. 따라서 memory allocation으로 입력 출력에 사용할 공간을 꼭 잡아두어야 한다. trt engine loading에는 입출력 메모리 할당도 무조건 같이 있어야 한다.
bindings = [int(d_input), int(d_output)]
stream = cuda.Stream()
def predict(batch): # result gets copied into output
# transfer input data to device
cuda.memcpy_htod_async(d_input, batch, stream)
# execute model
context.execute_async_v2(bindings, stream.handle, None)
# transfer predictions back
cuda.memcpy_dtoh_async(output, d_output, stream)
# syncronize threads
stream.synchronize()
return output
pred = predict(dummy_img)
predict 함수 같은 경우, 거의 고정 TensorRT API와 pycuda를 이용하여 동작시키는 코드라서 큰 변형이 없을 것으로 보인다. 다른 코드를 보면 뭔가 더 추가되는 부분이 많지만 위 형태가 가장 basic한 형태라고 보인다.
start = time.time()
pred = model(dummy_batch)
end = time.time()
# 0.1577s
start = time.time()
pred = predict(dummy_img)
end = time.time()
# 0.0036s
속도가 상당히 많이 줄어든 것을 볼 수 있다. 물론 resnet50처럼 구조가 간단한 점, precision을 낮추는 과정에서 같이 낮아진 성능을 무시한 점이 있지만 가속은 되었다.
Deploying to C++ API
이건 환경 설정부터가 쉽지 않다.
일단 C++ API를 쓰려면 환경부터 만들어야 하는데 docker를 쓰는 걸 무조건 추천한다. 공식 메뉴얼에서도 도커를 쓰는 방식을 주로 설명하므로 굳이 안 쓸 이유가 없다.
불가피하게 local에서 할 경우에는 CMakeLists.txt 부터 새로 작성해야 하는데 꽤 복잡하다. 이게 tar 파일로 설치했을 때 deb로 깔았을 때가 달라서 귀찮다.
직접 예제 삼아 작성한 CMakeLists.txt 파일은 다음과 같다.
cmake_minimum_required(VERSION 3.20) # 3.20 is not mandatory
project(tensorrt_cpp_tutorial)
# Use ccache to speed up rebuilds
include(cmake/ccache.cmake)
# Set C++ version and optimizatino level
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -Ofast -Wno-deprecated-declarations")
# For finding FindTensorRT.cmake
set(CMAKE_MODULE_PATH "${CMAKE_SOURCE_DIR}/cmake" ${CMAKE_MODULE_PATH})
set(CUDA_TOOLKIT_ROOT_DIR /usr/local/cuda)
find_package(OpenCV REQUIRED)
find_package(TensorRT REQUIRED)
find_package(CUDA REQUIRED)
include_directories(
${OpenCV_INCLUDE_DIRS}
${CUDA_INCLUDE_DIRS}
${TensorRT_INCLUDE_DIRS}
)
link_libraries(
${OpenCV_LIBRARIES}
${CUDA_LIBRARIES}
${CMAKE_THREAD_LIBS_INIT}
${TensorRT_LIBRARIES}
)
add_executable(run_inference
main.cpp
)
직접 로컬에서 할 경우 이 스크립트를 활용하는 걸 추천한다.
Issue
CMakeLists.txt 안에서 opencv랑 TensorRT를 못 찾는 일이 꽤나 빈번하다.
opencv의 경우, make까진 되지만 실제 코드에서 사용하려고 하면 opecnv2 not found 오류를 자주 본다.
sudo ln -s /usr/include/opencv4/opencv2 /usr/include/opencv2
이런 경우, opencv4 버전대로 넘어오면서 opencv2가 opencv4 하위 폴더로 이동했는데 경로 링크해주면서 해결하면 된다.
TensorRT의 경우, 위치를 알아서 직접 찾으면 좋지만 (tar 파일로 설치하고 bashrc에 경로를 제대로 export 해두었다면 바로 됐을 것) 그렇지 않다면 경로를 찾아내서 기록해줘야 한다. 이게 은근 귀찮고 컴퓨터 바뀔 때마다 CMakeLists.txt 내 경로를 바꿔줘야 할 수도 있으니 다음 cmake 파일을 활용하는 것을 추천한다.
프로젝트 하위에 cmake라는 폴더 하나 만들고 여기에 이 파일들 넣어두고 스크립트에서 불러오는 식으로 쓰면 된다. 알아서 위치 찾아준다. ( 참고글 에서 파일만 가져온 것이다.)
실제 동작 코드는 다음과 같다. (참고글)
#include <fstream>
#include <iostream>
#include <string>
#include <opencv2/opencv.hpp>
#include <opencv2/core/cuda.hpp>
#include <cuda_runtime.h>
#include "NvInfer.h"
#include "NvUffParser.h"
#include "NvCaffeParser.h"
#include "NvOnnxParser.h"
using namespace nvinfer1;
template <typename T>
struct TrtDestroyer
{
void operator()(T* t) { t->destroy(); }
};
template <typename T> using TrtUniquePtr = std::unique_ptr<T, TrtDestroyer<T> >;
class Logger : public ILogger
{
void log(Severity severity, const char* msg) noexcept override
{
// suppress info-level messages
if (severity <= Severity::kWARNING)
std::cout << msg << std::endl;
}
};
int main() {
Logger logger = Logger();
std::string enginePath = "/home/jseob/Desktop/yjs/codes/trt_tutorials/resnet_engine.trt";
std::ifstream engineFile(enginePath, std::ios::binary);
engineFile.seekg(0, engineFile.end);
long int fsize = engineFile.tellg();
engineFile.seekg(0, engineFile.beg);
std::vector<char> engineData(fsize);
engineFile.read(engineData.data(), fsize);
if (!engineFile)
{
std::cout << "Error loading engine file: " << enginePath << std::endl;
return 0;
}
TrtUniquePtr<IRuntime> runtime{createInferRuntime(logger)};
ICudaEngine* engine = runtime->deserializeCudaEngine(engineData.data(), fsize);
IExecutionContext* context = engine->createExecutionContext();
//inference
return 0;
}
이렇게 하면 기존에 만들어둔 trt engine을 loading하는 것까진 끝난다.
뒤이어 입력/출력 메모리를 할당하고 실행시키는 부분이다. ( 실패했음. 20240122)
void* buffers[2];
const char* inputName = "input.1";
const char* outputName = "495";
int batchSize = 8;
int dimC = 3;
int dimH = 224;
int dimW = 224;
float *input = (float *)malloc(batchSize*dimC*dimH*dimW * sizeof(float));;
const int inputIndex = engine->getBindingIndex(inputName);
const int outputIndex = engine->getBindingIndex(outputName);
cudaMalloc(&buffers[inputIndex], batchSize*dimC*dimH*dimW *sizeof(float));
cudaMalloc(&buffers[outputIndex], batchSize* 1000 * sizeof(float));
cudaStream_t stream;
cudaStreamCreate(&stream);
cudaMemcpyAsync(buffers[inputIndex], input, batchSize*dimC*dimH*dimW*sizeof(float), cudaMemcpyHostToDevice, stream);
context->enqueue(batchSize, buffers, stream, nullptr);
// context->enqueueV3(stream);
cudaStreamSynchronize(stream);
cudaStreamDestroy(stream);
cudaFree(buffers[inputIndex]);
cudaFree(buffers[outputIndex]);
}
loading 이후에 inference 코드는 어떤 방식이 가장 맞는 것인지 못 정리했다.
tensorrt 공식 github sample들은 업데이트가 제대로 안되는 것 같다. context->enqueue를 사용하는 방식인데 이거 deprecated 된다고 하는 걸보아 업데이트가 안되는 것 같다. enqueuV3까지 있는데...
아래 코드가 가장 최근까지 업데이트 되는 것 같아서 리뷰 후에 inference 내용은 정리할 것 같다.
https://github.com/cyrusbehr/tensorrt-cpp-api/tree/main
Issue
inference에 이런식으로 구현했을 때 오류는 안나던데 되는 건지는 잘 모르겠다.
3: [runtime.cpp::~Runtime::346] Error Code 3: API Usage Error (Parameter check failed at: runtime/rt/runtime.cpp::~Runtime::346, condition: mEngineCounter.use_count() == 1. Destroying a runtime before destroying deserialized engines created by the runtime leads to undefined behavior.
이런 오류가 코드 종료된 이후에 떠있는 걸 보아 뭔가 오류가 있는 것 같은데, 이건 전혀 다른 이슈인 것 같아서 확인이 필요함. 아무튼 loading까지만 성공했다.
'Knowhow > ONNX, TensorRT' 카테고리의 다른 글
[TensorRT 튜토리얼] 5. torch2trt (0) | 2024.01.23 |
---|---|
[TensorRT 튜토리얼] 4. How TensorRT works (0) | 2024.01.23 |
[TensorRT 튜토리얼] 3-1. ONNX/TRT engine conversion (0) | 2024.01.22 |
[TensorRT 튜토리얼] 3. TensorRT ecosystem (0) | 2024.01.19 |
[TensorRT 튜토리얼] 2. Docker container (0) | 2024.01.19 |