테크매니아
PyTorch, onnx, ncnn 성능 비교 본문
개요
이전 ncnn 포스팅에 이어서 모델 배포에 대한 실험을 했습니다. PyTorch로는 모델 연구를 하고 TFLite나 onnx로 모델을 배포합니다. 그걸 On-Device에서 더 빠르게 하기 위한 프레임워크가 ncnn인데요, 이것들의 성능 차이가 얼마나 나는지에 대한 실험입니다.
실험 모델 : ResNet152
실험에 사용하는 모델은 ResNet입니다. 주요 모델의 백본으로 많이 사용되는 모델이지요. MNIST cnn으로 간단하게 테스트 하려고 했는데 일반 cnn 모델은 너무 빨라서 성능 비교가 어려워서 ResNet을 사용했습니다.
제가 개인적으로 좋아하는 ResNet18모델도 역시 빨라서 가장 무거운 resnet152을 테스트에 사용했습니다.
실험에만 집중하기 위해서 모델을 만들지 않고 Torch Hub에 있는 모델을 참고 했습니다. 간단한 샘플 코드도 있고, imagenet으로 학습됐습니다.
Ref : https://pytorch.org/hub/pytorch_vision_resnet/
실햄 배경
- 실험 환경
- CPU : i5-1035G4 CPU
- MEM : 16GB
- Torch : torch==1.10.2+cpu (CPU 버전을 썼습니다.)
- onnx : 1.10.2
- onnxruntime 1.10.0
- ncnn : 1.0.20211208
실험 : PyTorch
# sample execution (requires torchvision)
import time
from PIL import Image
from torchvision import transforms
input_image = Image.open(filename)
preprocess = transforms.Compose([
transforms.Resize(256),
transforms.CenterCrop(224),
transforms.ToTensor(),
transforms.Normalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225]),
])
input_tensor = preprocess(input_image)
input_batch = input_tensor.unsqueeze(0) # create a mini-batch as expected by the model
print(fInput shape : {input_batch.shape})
print(fInput : {input_batch[0][0][0][:7]})
# move the input and model to GPU for speed if available
if torch.cuda.is_available():
input_batch = input_batch.to('cuda')
model.to('cuda')
test_loop = 50
inference_time_all = 0
for _ in range(test_loop):
with torch.no_grad():
start_time = time.time()
output = model(input_batch)
inference_time = float(time.time() - start_time)
inference_time_all += inference_time
print(fFPS : {1/inference_time_all*test_loop})
- PyTorch로 inference한 코드 조각 입니다.
- 전처리로는 Pillow imge로 read하고 256으로 Resize, 224로 CenterCrop, Normalize합니다.
- PyTorch model을 바로 inference합니다.
- 전후처리 제외하고 inference만 50회 반복했고 평균 FPS를 측정한 결과 10 FPS 정도로 나왔습니다.
ONNX Export
위에 Torch로 만들어진 pth 모델을 onnx로 Export 했습니다. 입력 size를 정해주고 tensor를 하나 만들어서 onnx로 export 하면 됩니다.
dummy_input = torch.randn(1, 3, 224, 224)
input_names = [ input1 ]
output_names = [ output1 ]
torch.onnx.export(model, dummy_input, resnet.onnx, verbose=True, input_names=input_names, output_names=output_names)
참고로 Torch의 resnet152.pth파일 크기는 231MB였는데, onnx로 export 하니 resnet.onnx : 230MB가 됐습니다. 1MB정도 차이가 나는데 메타데이터 정도가 빠진게 아닌가 싶습니다. onnx 자체는 모델 압축이나 변경이 없으니 거의 같다고 보는게 맞겠습니다.
실험 : onnxruntime
import onnxruntime as ort
import numpy as np
import time
ort_sess = ort.InferenceSession('resnet.onnx')
test_loop = 50
inference_time_all = 0
for _ in range(test_loop):
start_time = time.time()
output = ort_sess.run(None, {'input1': input_batch.numpy()})
inference_time = float(time.time() - start_time)
inference_time_all += inference_time
print(fFPS : {1/inference_time_all*test_loop})
# Print Result
output = np.array(output)
output = torch.Tensor(output)
onnx로 inference한 코드입니다. PyTorch와 거의 비슷합니다. 전처리도 동일하게 사용했습니다. 다른게 있다면 (당연히) Torch Tensor가 아니라 ndarray를 입력으로 넣고 inference 했습니다.
추론 시간을 측정해 보면.. 알 수 없는 결과가 나왔습니다. 최소 8FPS ~ 17FPS로 성능이 왔다 갔다 합니다. 반복 실험을 해보면 hot 메모리 캐시 hit 때문에 시간 차이가 나는 것 같지도 않고.. 일정하지 않습니다.
실험에 jupyterlab을 사용했는데 kernel을 초기화 하고 반복해 봐도 비슷한 결과가 나옵니다.
그래도 9FPS에 근접하게 나오는 거 같습니다.
ncnn 모델 변환
onnx 모델을 ncnn 모델로 변환해야 한다. ncnn은 메뉴얼에 따라 설치하면 되고, Build후
./onnx2ncnn ~/notebook/resnet.onnx resnet.param resnet.bin
단순히 모델을 변환 하는건 크게 복잡하지 않습니다.
참고로 resnet.bin 파일은 230MB입니다. 역시 압축하지 않아서 원본 모델의 정보를 모두 갖고 있는 것 같습니다.
실험 : ncnn
ncnn은 기본적으로 c++으로 구현돼 있지만 pybind11에 의해 Python으로 wrapping돼 있습니다. pypi에 올라가 있어서 pip로 간단히 install 할 수 있습니다.
python -m pip install -U ncnn
GitHub에 관련 샘플이 있어 이걸 보고 비슷하게 추론부 코드를 만들어서 테스트 할 수 있습니다.
import sys
import cv2
import numpy as np
import ncnn
import time
from PIL import Image
from torchvision import transforms
class ResNet:
def __init__(self, use_gpu=False):
self.use_gpu = use_gpu
self.net = ncnn.Net()
self.net.opt.use_vulkan_compute = self.use_gpu
# model is converted from
self.net.load_param(./resnet.param)
self.net.load_model(./resnet.bin)
self.mean_vals = [0.485 * 255, 0.456 * 255, 0.406 * 255]
self.norm_vals = [1 / 0.229 / 255., 1 / 0.224 / 255., 1 / 0.225 / 255.]
def __call__(self, img):
img = cv2.resize(img, dsize=(256, 256), interpolation=cv2.INTER_AREA)
input_batch = ncnn.Mat.from_pixels_roi(img, ncnn.Mat.PixelType.PIXEL_BGR, 256, 256, 16, 16, 224, 224)
input_batch.substract_mean_normalize(self.mean_vals, self.norm_vals)
print(fInput shape : {input_batch.w}, {input_batch.h})
print(fInput : {input_batch[0]} {input_batch[1]} {input_batch[2]})
ex = self.net.create_extractor()
ex.input(input1, input_batch)
start_time = time.time()
ret1, mat_out1 = ex.extract(output1)
inference_time = float(time.time() - start_time)
mat_out1 = np.array(mat_out1)
print(finference_time : {1/inference_time})
return mat_out1
대부분의 소스는 다른 코드를 참조했습니다. Torch, onnx와 다르게 전처리, 후처리도 ncnn에 있는걸 사용 했습니다. 원래 전처리는 Input tensor를 256으로 resize, 224로 crop, normalize순서로 처리했습니다. torch와 완벽 호환되지는 않아서 resize는 opencv에 있는 resize를, crop은 ncnn의 from_pixels_roi을 사용했고, normalize도 ncnn의 substract_mean_normalize를 사용했습니다. 특히 mean_vals에는 *255, norm_vals 에는 /255를 했습니다.
실험 결과 ncnn은 약 10 FPS정도로 나왔습니다.
결과
ResNet152에 대해서 PyTorch, onnx, ncnn에서 추론 시간을 비교 했습니다.
추론 시간만 측정했고, 실제로는 전/후처리, +@ 최적화를 모두 고려해야 정확하겠습니다. 아주 정확한 실험이 아니라 오차가 있을 수 있지만 대략적인 성능은 아래와 같습니다.
Model(ResNet152) | PyTorch | onnx | ncnn |
---|---|---|---|
Weight file size | 231M | 230M | 230M |
FPS | 10 FPS | 9 FPS | 10 FPS |
Weight file size가 똑같아서 (압축하지 않아서) 그런지 성능은 10FPS로 거의 비슷했습니다. GPU를 사용하지 않아서 그런지 (아무래도 cpu 최적화는 별로 안했을듯) 비슷했습니다. operator 최적화나 이런저런 것들을 최적화 했을 줄 알았는데.. 비슷한 결과가 나왔습니다. 다음에는 ncnn의 quantization을 사용해서 시간과 인식율 비교를 실험해 보겠습니다.
참고 notebook : https://gist.github.com/dankernel/6767149ba890fe7dfd0e7fe519825bd8