안녕하세요. LINE+ Camera AI 팀의 손재범입니다. Camera AI 팀에서는 주로 비전 문제를 다루며 숏폼 플랫폼과 카메라 앱에서 사용하는 AI 적용 콘텐츠를 연구하고 개발합니다. 예를 들어, 카메라에 비친 내 얼굴을 만화 캐릭터처럼 바꿔주는 애니메이션 필터 기능이나 립싱크 기능과 같은 신기하면서 재미있는 카메라 필터를 연구하고 개발하는 팀입니다. 또한, LINE 앱에서 쉽게 사용할 수 있도록 AI 기능을 최적화하고 경량화하는 일도 병행하고 있습니다.
저희 팀에선 서버 환경이 아닌 모바일 환경에서 AI 모델을 추론해야 하는 상황이 자주 있는데요. 이에 모바일 GPU 환경에서 '최소한의 성능 감소와 최대한의 속도 향상'을 탐색하는 과정에서 세운 여러 가지 가설과 이를 증명하기 위한 실험 내용을 공유하면 좋겠다는 생각이 들어서 글을 쓰게 되었습니다. 비록 NVIDIA GPU와 Apple 칩셋 하나씩만 사용해 진행했지만, 개별 연산 단위에서부터 여러 연산으로 구성한 모델까지 실험하고 비교했습니다. 저희가 세운 가설과 실험 그리고 결론을 아래와 같은 순서로 공유해 보려고 합니다.
- 모바일과 데스크톱 GPU의 스펙을 간단히 비교합니다.
- 뉴럴 네트워크에서 자주 사용하는 연산들의 속도를 간단히 비교하고 결과를 분석합니다.
- 널리 사용되는 백본 네트워크들을 모바일 GPU에서 실행하고 그 결과를 분석합니다.
실험으로 비교하는 종목은 'RTX3090 Pytorch'와 'A13 TFLite', 그리고 'A13 CoreML'입니다. 성능을 최적화하기 위한 연산자 분석과 데스크톱과 모바일 GPU를 비교하기 위한 프레임워크로는 아래와 같은 이유로 각각 Pytorch와 TFLite, CoreML을 사용했습니다.
- Pytorch: Tensorflow를 포함한 다른 딥러닝 학습 프레임워크보다 사용하기 편하면서 Python과 잘 어울리고 저희 팀을 포함한 많은 AI 연구원들이 이용하고 있습니다.
- TFLite: 참고할 수 있는 문서와 자료가 많고, Android와 iOS 모두에서 사용할 수 있고 훌륭한 추론 성능을 보여줍니다.
- CoreML: Apple 기기에서 사용할 수 있습니다.
NVIDIA GPU와 모바일 GPU 사양 간단 비교
아래는 RTX3090과 A13 Bionic 칩셋(iPhone SE2)의 사양을 간단하게 비교한 표입니다. RTX3090는 사양과 관련 자료가 상세히 공개돼 있지만 Apple 칩셋들은 그렇지 않습니다. 이에 비교적 쉽게 살펴볼 수 있는 사양들만 비교했습니다.
RTX3090 | A13 Bionic(iPhons SE2) | |
---|---|---|
GPU 기본 클록 | 1.40 - 1.70 GHz | 1.35 GHz |
Shading units(cuda cores) | 10,496 | 256 |
트랜지스터 | 283억 | 85억 |
NPU(Neural Processing Unit) | 328 TensorCore | 8 Neural engine (TFLite x) |
메모리 | 24GB | 시스템 메모리 공유 |
Performance FP32(Float) | 35.5 TFLOPS(A13 약 51배) | 691.2 GFLOPS |
연산별 비교
이번 글에서는 기기와 프레임워크 간 성능을 비교하고 어떤 파라미터와 레이어 구성이 모델을 더 효율적으로 설계하는 데 도움이 될지 알아보기 위해 진행한 여러 가지 실험을 소개할 예정입니다. 비교하기 위해선 먼저 비교에 사용할 기준을 정해야 하는데요. 이번 글에서는 초당 수행한 부동 소수점 연산의 개수를 계산하는 FLOPS와 추론에 걸린 시간을 평균 낸 지연시간(latency, ms)을 사용하겠습니다.
자주 사용하는 연산자 비교
우선 A13과 NVIDIA GPU의 성능 차이를 대략적으로 가늠하기 위해서, 자주 사용하는 연산 몇 개를 골라 같은 입력과 같은 파라미터 조건에서 단순히 지연 시간을 비교해 보는 실험을 진행했습니다. 두 기기 간의 대략적인 성능 차이를 보기 위한 이번 실험에서는 자주 사용하는 몇 가지 연산자들을 샘플링했는데요. 이번 실험에서는 FLOPS라는 기준 대신 지연 시간을 사용해 비교하겠습니다. 메모리를 읽고 쓰는 시간이 대부분인 ReLU(Rectified Linear Unit)나 전치(transpose)와 같은 레이어들과 지수 연산이 들어가는 몇몇 활성 함수(activation function)에서는 FLOPS를 사용하기 어렵기 때문입니다.
또한, NVIDIA GPU와 iPhone SE2 칩의 코어를 최대한 활용할 수 있는 크기의 연산을 실행하려 했으나, 입력 크기가 너무 크면 상대적으로 메모리가 적은 모바일 환경에서는 실험 진행이 불가능했습니다. 또한 단일 연산 모델을 실행할 때에는 기기와 TFLite 프레임워크의 구조 때문이라고 추정되는 성능 왜곡 현상이 발생하기 때문에 작은 연산을 3개 층으로 연달아 쌓아 성능을 측정했습니다. 두 기기 간 비교에 사용한 입력 형태는, 선형(linear) 레이어에서는 (4, 4096) 텐서를 사용했고, 나머지 레이어에서는 (1, 32, 32, 128) 텐서를 사용해 속도를 측정했습니다.
Conv2d | ReLU | Linear | BatchNorm | LayerNorm | Upsample(Bilinear) | Upsample(Nearest) | Transpose | |
---|---|---|---|---|---|---|---|---|
RTX3090 Pytorch 지연 시간(ms) | 0.1678 | 0.0171 | 0.2775 | 0.04438 | 0.4516 | 0.1403 | 0.0731 | 0.00579 |
A13 TFLite 지연 시간(ms) | 3.75897 | 1.94035 | 6.89408 | 1.97055 | 7.28187 | 29.4306 | 29.7508 | 2.21463 |
A13 CoreML 지연 시간(ms) | 3.0794 | 1.3940 | 6.218 | 1.402174 | 3.21639 | 11.3971 | 7.16235 | 2.401145 |
단일 레이어로 구성된 TFLite 모델 중에서 특히 수행해야 할 연산이 적거나 아예 없는 ReLU와 전치 레이어는 데스크톱 GPU보다 모바일에서의 성능이 낮았습니다. 지연 시간을 측정하기 전에 모바일 GPU를 초기화하는 과정과 웜업 세션을 수차례 거쳤지만, 그럼에도 입력 텐서를 GPU로 옮기는 과정에서 많은 시간이 소요된 것으로 보입니다. 2차원 합성곱(Convolution2D, Conv2d)과 선형 레이어 또한 데스크톱 GPU에 비해 모바일에서 속도가 20배 이상 느렸으나, LayerNorm 레이어에서는 데스크톱 GPU와의 성능 차이가 줄어들었습니다. 기기 문제라기보단 알고리즘 문제로 보입니다. 위 표에 실험 결과로 추가하진 않았지만 같은 연산을 모바일에서 GPU가 아닌 CPU로 진행했을 때 2차원 합성곱과 선형 레이어를 제외한 다른 연산은 오히려 속도가 빨랐는데요. 이와 같은 결과를 고려해 볼 때 처음 연산에서 CPU ↔ GPU 간 데이터 스위칭에 많은 시간이 소요된 것으로 보입니다.
코어의 처리 능력 외에도 성능에 영향을 미치는 변수가 많아 두 기기의 성능을 정확하게 비교하는 것은 어렵습니다. 다만, 입출력의 크기가 지나치게 크지 않고, 연산량이 많은 선형 레이어의 속도 차이가 약 20배 정도였는데요. 저는 이를 두 기기 간의 성능 차이로 보려고 합니다. 물론 RTX3090 기기가 TensorCore라는 행렬곱에 특화된 특성 덕분에 가속 효과를 받았고, NVIDIA GPU가 최대한으로 활용되기엔 연산의 크기가 적다는 점은 감안해야겠지만요.
선형 레이어
이번 실험부터는 모바일 GPU에서의 레이어들의 성능을 분석하고, GPU 활용률을 증가시키는 조건을 찾아보며 정리하려고 하는데요. 먼저 두 개의 행렬을 곱하는 행렬곱으로 구성된 선형 레이어부터 살펴보겠습니다.
선형 레이어는 2차원 형태의 입력과 2차원 형태의 파라미터를 곱하는 행렬곱 연산입니다. 이와 같은 행렬곱은 뉴럴 네트워크에서 가장 기본적인 연산 중 하나인데요. 자연어 처리에서 주로 사용하는 트랜스포머(transformer) 구조에서도 행렬곱을 주 연산으로 사용하며, 영상 처리 분야에서 사용하는 CNN(Convolutional Neural Network)도 내부 합성곱(convolution) 연산을 행렬곱으로 바꿔 수행할 수 있습니다. 선형 레이어 실험은 (MxK)와 (KxN) 행렬곱 연산에서 세 파라미터 M, N, K를 바꿔가며 파라미터를 어떻게 조정했을 때 가장 좋은 성능이 발휘되는지 알아보는 형태로 진행하겠습니다. 여기서 M, N, K는 각각 배치 크기(batch_size)와 입력 차원(input_dimension), 출력 차원(output_dimension)을 의미합니다.
우선 배치 크기에 해당하는 M 파라미터를 1로 고정한 뒤 M 과 N 중에서 하나의 파라미터만 증가시키며 성능을 측정해 봤습니다. 아래 실험 결과 그래프를 보시겠습니다. 파란색 선이 N을 고정했을 때의 결과이고, 빨간색 선이 K를 고정했을 때의 결과입니다.

M과 K 두 파라미터 중 어느 하나만 증가할 때는 뚜렷한 성능 차이가 관측되지 않았습니다만, 한 번에 수행하는 연산의 크기가 증가하는 상황에서는 K가 커질 때보다는 N이 커질 때가 조금 더 성능이 좋았습니다. 또한 실험에서는 연산의 크기가 작을수록 FLOPS가 급격히 떨어지는 모습을 보였는데요. 모바일 환경이라는 특성 때문에 큰 연산을 사용하기 쉽지 않은 상황에서 작은 연산의 효율도 좋지 않다는 아쉬운 점이 관측됐습니다. 지연 시간 측면에서도 극단적으로 작은 크기의 연산은 장점이 없다고 생각하기 때문에 GPU를 사용해 추론할 때는 너무 작지 않은 레이어 크기를 유지하는 것이 좋을 것이라고 예상합니다.
다음으로 선형 레이어 행렬곱 연산시 TFLite와 모바일 GPU 구조의 특성 때문에 발생할 수 있는 차원 양자화(dimension quantization) 문제가 있는지 확인하기 위한 실험을 진행했습니다. GPU에서 연산을 수행할 때는 병렬화하기 위해 같은 명령어를 여러 스레드로 나누어 수행하는데 이때 동시에 실행되는 스레드 숫자의 최소치가 생깁니다. 예를 들어 최소치가 4개라고 가정한다면, 길이가 3인 벡터 두 개를 내적할 때 실제로는 나머지 부분이 자동으로 0으로 패딩돼 길이가 4인 벡터 두 개의 내적 연산을 수행하는 것과 같이 동작합니다. 이를 차원 양자화 문제라고 합니다.
이와 같은 차원 양자화 문제가 TFLite에 존재하는데요. 이것이 결과에 얼마나 큰 영향을 미치는지 확인하고자 N과 K 중 하나를 4096으로 고정하고, 나머지 파라미터를 248부터 256까지 하나씩 증가시키며 실험을 진행했습니다. 아래는 그 결과 그래프입니다.

실험 결과, 실제로 체감할 수 있을 정도의 문제는 아닌 것으로 나타났습니다. 비슷한 실험을 더 작은 연산으로도 진행해 보았으나 유의미한 성능 감소는 관측할 수 없었습니다.
2차원 합성곱
선형 레이어와 마찬가지로 합성곱 레이어 역시 특정 파라미터만 조절하는 실험을 진행해 어떤 파라미터를 어떻게 설정하는 것이 성능 향상에 좋을지 관찰해 보기로 했습니다. 이번 실험에서는 H와 W, in_channel, out_channel의 네 가지 파라미터를 조절하며 어떤 파라미터가 성능에 더 민감하게 영향을 끼치는지 결과를 관찰했습니다. 각 파라미터는 순서대로 입력 텐서의 높이(H)와 너비(W), 채널 수(in_channel), 그리고 합성곱 필터의 개수(out_channel)를 의미합니다. 이번 비교 실험에서 합성곱 커널 크기는 3, 패딩은 0, 스트라이드(stride)는 1로 통일했습니다. 아래 결과 그래프를 보시겠습니다.

실험 결과 H와 W 파라미터가 커질 때 전체적으로 성능이 저하됐습니다. 크기가 큰 연산을 수행할 때 입력 텐서의 높이와 너비가 큰 경우보다는, 입력 텐서의 채널 수가 크거나 합성곱 필터의 개수가 많아 출력 텐서의 채널이 큰 경우에 같은 연산량 대비 성능이 좋은 것을 관측할 수 있었습니다.
이어서 이전 실험에서는 진행하지 않았던, 커널 크기 파라미터를 조절하며 성능 변화를 관찰하는 실험도 진행했습니다. 실험에 사용하는 연산들이 최대한 비슷한 크기가 되도록 H와 W를 조절해가며, 네 가지 커널 크기(3, 5, 7, 9)로 실험을 진행했습니다.
커널 크기 | 1 | 3 | 5 | 7 |
---|---|---|---|---|
H, W | 128 | 44 | 26 | 18 |
채널 수 | 512 | 512 | 512 | 512 |
A13 TFLite 지연 시간(ms) | 53.3681 | 15.0811 | 18.9144 | 16.678 |
A13 CoreML 지연 시간(ms) | 13.8979 | 8.4727 | 10.3622 | 9.9478 |
FLOPs | 약 85억 | 약 91억 | 약 88억 | 약 83억 |
실험 결과 커널 크기가 커질수록 성능이 좋아지는 경향이 나타났는데요. 특이하게도 커널 크기가 3일 때 가장 성능이 좋았습니다. 이는 TFLite에서 3x3 필터를 사용하는 합성곱 연산이 Winograd 합성곱 알고리즘의 가속을 받아 성능이 크게 향상된 것으로 보입니다. Winograd 합성곱 알고리즘은 특정 커널 크기에서 일반적으로 사용하는 행렬곱 기반의 합성곱보다 수행해야 할 곱셈 연산을 현저히 줄여주는데요. TFLite에서는 4배 정도 곱셈 연산이 줄어든 Winograd 알고리즘을 사용하고 있습니다.
다음으로 패딩 유무에 따른 성능 변화가 있는지 확인하고자 추가 실험을 진행했는데요. H와 W는 32, in_channel과 out_channel은 64로 고정한 작은 연산과, 파라미터의 크기가 두 배인 큰 연산으로 나누어 진행했습니다. 또한 커널 크기 파라미터를 변경해가며 패딩이 들어간 경우는 입력 텐서의 높이 및 너비와 같은 크기를 가진 출력 텐서가 생성되도록 패딩 파라미터를 조절했습니다. 아래는 실험 결과 그래프입니다.

실험 결과 커널 크기가 3인 경우에 성능이 가장 좋았고, H와 W가 작고 커널 크기가 커서 패딩이 많이 들어간 경우에는 성능이 상당히 저하됐습니다. 패딩 유무에 따른 지연 시간 차이는 큰 연산과 작은 연산 모두에서 관측됐지만, 특히 작은 합성곱 연산에서 커널 크기가 커질 때 지연 시간이 많이 증가하는 것을 확인할 수 있었습니다.
다음으로 입출력 채널 크기의 비율이 1:1에서 점점 벗어날 때 메모리 접근 비용이 증가하는 것을 TFLite GPU에서도 관측할 수 있는지 실험했습니다. 실험에서 H와 W 파라미터는 64로 고정하고, 커널 크기 또한 3으로 고정했습니다. 또한 최대한 비슷한 FLOPs를 유지해 가며 채널 비율이 1:1에서 벗어나는 2차원 합성곱 레이어를 하나 생성하고 속도를 측정했습니다.
H, W | In channel | Out channel | A13 TFLite 지연 시간(ms) | RTX3090 지연 시간(ms) |
---|---|---|---|---|
64 | 128 | 128 | 6.376 | 0.0612 |
64 | 90 | 180 | 5.446 | 0.0884 |
64 | 52 | 312 | 6.519 | 0.1246 |
64 | 36 | 432 | 7.828 | 0.1280 |
RTX3090은 채널 비율이 1:1에서 벗어나는 만큼 지연 시간이 선형적으로 증가하는 모습을 보였지만, TFLite GPU에서는 입출력 채널의 비율이 1:2일 때 가장 성능이 좋았습니다. 다만 비율이 1:1에서 크게 벗어나면 연산 수행 비용이 증가하는 것 또한 확인할 수 있었습니다.
메모리 레이아웃 변환
다음으로 메모리 레이아웃을 변환할 때의 지연 시간을 측정해 봤습니다. 특별히 TFLite의 공식 가이드라인에도 GPU에서의 메모리 레이아웃 변환은 느린 연산이기 때문에 가급적이면 사용을 자제하라고 언급하고 있는데요. 실제로 세 가지(전치, Reshape, PixelShuffle) 레이어에서 속도가 얼마나 느린지 확인하기 위해 아래와 같은 연산을 진행하며 지연 시간을 측정했습니다.
- (1, 64, 64, in_channel) 형태의 입력 텐서를 전치 연산으로 (1, in_channel, 64, 64) 형태의 텐서로 변환
- (1, 64, 64, in_channel) 형태의 입력 텐서를 Reshape 연산으로 (in_channelx64x64, 1) 형태의 텐서로 변환
- (1, 64, 64, in_channel) 형태의 입력 텐서를 PixelShuffle 연산으로 (1, 128, 128, in_channel/4) 형태의 텐서 변환
결과는 아래와 같습니다. 단일 레이어 모델을 TFLite에서 추론할 때 초기화 오버헤드가 있는 것처럼 보여 실험 결과에 왜곡이 있을 수 있으나, PixelShuffle과 전치 연산에 비해 Reshape 연산이 비교적 비용이 높게 나타났습니다.

우선 세 레이어 모두 작은 텐서 변환에서조차 2ms 이상 소요되는 것으로 나타났으며, 텐서의 크기가 클수록 변환하는 데 오래 걸렸습니다. 특히 Reshape 연산은 다른 두 레이어에 비해서 훨씬 오래 걸리는 것으로 나타났습니다.
모델 아키텍처와 디자인별 성능 측정
다음으로 세 가지 종류의 네트워크를 변환해 TFLite GPU로 실행 시간 및 모바일 기기에서의 성능을 측정했고, 비교하기 위해 CoreML과 Nvidia GPU에서의 지연 시간도 측정해 봤습니다. 실험할 때는 모바일로 한정 짓지 않고 다른 분야에서도 백본 네트워크로 많이 사용하는 Resnet 계열과, MobileNet 계열, 그리고 Efficientnet 계열의 모델로 실험했습니다.
모델 | resnet34 | resnet50 | resnet101 | mobilenet_v2 | mobilenet_v3_large | mobilenet_v3_small | efficientnet_b0 | efficientnet_b1 | efficientnet_b2 | efficientnet_b3 |
---|---|---|---|---|---|---|---|---|---|---|
RTX3090 Pytorch 지연 시간(ms) | 3.497 | 4.767 | 9.398 | 3.775 | 4.444 | 3.538 | 5.798 | 8.264 | 8.366 | 9.546 |
A13 TFLite 지연 시간(ms) | 31.8827 | 32.4437 | 50.1975 | 9.43992 | 10.1454 | 5.805 | 24.1116 | 34.5493 | 49.2367 | 60.753 |
A13 CoreML 지연 시간(ms) | 8.413 | 9.882 | 12.5296 | 2.542 | 18.184 | 6.494 | 21.496 | 24.560 | 37.799 | 49.358 |
TFLite 그래프 노드 개수 | 90 | 91 | 202 | 83 | 161 | 147 | 320 | 457 | 457 | 517 |
FLOPs | 약 36억 | 약 41억 | 약 78억 | 약 3.2억 | 약 2.2억 | 약 0.6억 | 약 4억 | 약 7.1억 | 약 10억 | 약 19억 |
EfficientNet과 MobileNet은 비교적 최근에 등장한 네트워크지만, 나온 지 더 오래된 단순한 구조의 ResNet이 GFLOPS 측면에서 훨씬 나은 결과를 보여줬습니다. 이와 같은 결과가 나온 원인을 유추하기 위해 TFLite 연산 그래프 노드 위 숫자를 확인해 보았는데요. 아무래도 구조가 더 복잡한 MobileNet과 EfficientNet 계열의 모델이 FLOPs 대비 노드의 숫자가 많은 경향을 보였습니다. 또한 EfficientNet과 MobileNet은 TFLite에서 큰 가속 효과를 받는 3x3 합성곱 커널 대신 5x5와 1x1을 주로 사용하고, TFLite 모델로 변환할 때 사용된 컨버터가 ReLU를 제외한 다른 활성 레이어 퓨즈 기능을 지원하지 않았기 때문에 전체적으로 빠른 성능을 발휘하지 못한 것으로 보입니다. 그에 비해 ResNet 계열의 모델은 3x3과 1x1 합성곱 커널을 주로 사용했고, 각각의 레이어가 비교적 큰 연산을 수행하기 때문에 좋은 성능을 보여준 것으로 확인됩니다.
모바일 GPU 환경에서 백본 네트워크 성능을 분석하다 보니 TFLite 그래프의 노드 숫자가 성능과도 관련이 있겠다고 추측할 수 있었습니다. 이미 ShuffleNet v2 모델을 제안한 논문에서 여러 개의 작은 레이어를 많이 사용하는 것보다 큰 레이어를 하나 사용하는 게 속도 측면에서 더 효율이 좋다고 보고된 바 있습니다. 추측을 확인하기 위해 총 연산 횟수를 최대한 비슷하게 유지하면서 입출력 채널 수와 H, W 파라미터를 조절하며 레이어 숫자를 늘린 모델을 생성해 지연 시간과 FLOPS를 측정해 보았습니다. 커다란 하나 혹은 소수의 연산과, 다수의 작은 연산의 효율을 비교해 본 실험 결과는 아래와 같습니다.

NVIDIA GPU에서는 노드의 숫자가 늘어날수록 성능이 감소한다는 것을 관측할 수 있었습니다. 하지만 모바일 GPU에서는 레이어의 개수가 1이나 2인 얕은 깊이의 네트워크에서는 그런 경향이 나타나지 않았고, 레이어 개수 3개 이상부터는 다소 노이즈가 섞여 있지만 레이어 숫자가 늘어날수록 성능이 감소하는 현상을 관측할 수 있었습니다. 이 결과가 수백 개의 레이어로 구성된 모델을 대표할 수는 없겠지만, 수행해야 할 총 연산 횟수가 적고 합성곱 커널의 크기가 최적화되지 않았으며 노드 수가 비교적 많은 MobileNet과 EfficientNet 계열의 모델은 GPU에서 효율이 좋지 않을 수 있다는 것을 알 수 있었습니다. 추가로 MobileNet v2는 TFLite CPU에서 XNNPACK을 사용하고 스레드 숫자를 늘려 가속 효과를 받으면, GPU 가속을 사용했을 때 보다 모델 추론을 1.3배에서 최대 1.7배까지 더 빠르게 할 수 있었고, Apple CoreML을 사용할 경우엔 지연 시간 3ms 이하의 빠른 성능을 보여줬습니다. 이처럼 작고 복잡한 네트워크에서는 GPU보다 CPU와 전용 장비의 가속을 받는 것이 더 효율적이었습니다.
CoreML의 경우 다른 GPU와 프레임워크 대비 MobileNet v2와 ResNet 계열에서 굉장히 빠른 속도를 보여줬습니다. 이에 CoreML이 특별한 구조에 최적화돼 있어서 빠르게 동작하는 게 아닌지 궁금해졌는데요. 이를 확인하기 위해 MBConv 블록(EfficientNet)과 Inverted Residual 블록(MobileNet v2), Residual 블록(ResNet)으로 블록 단위 테스트를 진행했습니다. 블록의 입력 텐서로는 (1, 112, 112, 24) 형태의 텐서를 사용했습니다. 아래는 각 블록을 시각화한 그림입니다. 그림에서 볼 수 있듯 EfficientNet 블록인 MBConv 블록이 다른 두 블록보다 노드가 많고 구조가 복잡합니다.

아래는 RTX3090와 iPhone SE2(CoreML, TFLite GPU)에서 각 블록 추론 속도를 측정한 결과입니다.
EfficientNet(MBConv 블록) | MobileNet v2(Inverted Residual 블록) | ResNet(Residual 블록) | |
---|---|---|---|
FLOPs | 약 1.1억 | 약 1.1억 | 약 1.3억 |
TFLite 그래프 노드 개수 | 16 | 5 | 5 |
RTX3090 Pytorch 지연 시간(ms) | 0.3400 | 0.2088 | 0.1515 |
A13 TFLite 지연 시간(ms) | 5.8261 | 4.27251 | 4.10687 |
A13 CoreML 지연 시간(ms) | 9.4856 | 2.53574 | 1.87518 |
TFLite와 CoreML, RTX3090 모두 MBConv 블록을 추론할 때 가장 오래 걸렸는데요. 특히 CoreML에서 MBConv 블록을 추론할 때 Residual 블록을 추론할 때보다 5배 정도 많은 시간이 걸렸습니다. RTX3090에서의 실험도 CoreML 실험과 비슷했지만 성능 차이의 폭은 더 좁았습니다. 원인을 따져 보면, 가장 구조가 복잡한 MBConv 블록에서 노드 숫자가 가장 많았을뿐더러 연산 그래프에서 연산이 직렬로 구성되지 않고 부분적으로 나누어지고 합쳐지는 과정이 많았는데요. 이 때문에 동기화 이슈와 같은 간접 비용이 증가해서 이런 결과가 나온 것이 아닐까 생각합니다.
CoreML의 MobileNet v2 추론 시간이 굉장히 빨랐기 때문에 혹시 모델 단위로 최적화돼 있는 것은 아닌지 궁금했습니다. 이에 단순히 프로파일러를 사용해 지연 시간 분포를 확인하는 게 아니라 블록마다 모델을 만드는 실험을 진행했습니다. 구체적으로 MobileNet v2 모델을 20개의 블록으로 나누고, 첫 번째 블록부터 층층이 쌓아나가며 지연 시간을 측정했는데요. 다만, 실험의 특성 때문에 환경을 통제하기가 쉽지 않았고, CoreML은 CPU와 GPU, 뉴럴 엔진 중 어떤 유닛을 계산에 사용할지 알 수 없어서 실험 결과에 약간의 노이즈가 나타났습니다.

이번 실험은 TFLite GPU에서도 같이 진행했습니다. 블록 개수를 쌓아갈 때마다 점점 지연 시간이 증가하는 것으로 보아 완성된 MobileNet v2 자체에 특별히 최적화하지는 않은 것으로 보입니다. 하지만 CoreML과 TFLite GPU 모두 첫 번째 블록이 전체 모델의 연산 횟수 중에서 4% 미만의 크기를 차지함에도 불구하고 첫 번째 블록을 추론하는 데 걸리는 지연 시간이 연산량 대비 높게 나타났습니다. 이런 결과로 미루어 짐작해 볼 때, 모델 추론 시 초기화 오버헤드가 존재한다고 유추할 수 있고, 이것이 단일 연산 레이어를 실행할 때 비정상으로 보일 만큼 성능이 떨어지는 이유가 될 것이라고 생각합니다. 또한 CoreML의 추론 속도는 A13 칩 뉴럴 엔진의 프로세서 유닛 자체가 뉴럴 네트워크에 최적화됐기 때문에 셰이더(shader) 프로그램으로 실행되는 TFLite GPU보다 빨랐습니다. 하지만 모든 네트워크나 블록에서 항상 빠른 것은 아니므로 특정 구조나 연산에 최적화돼 있는 것으로 생각됩니다.
마지막으로 비슷한 실험을 EfficientNet b0에서도 반복했습니다. 이번에는 CoreML만 사용해서 실험을 진행했으며, MobileNet v2 실험과 비슷하게 CoreML을 사용해 EfficientNet b0 네트워크를 블록 단위로 나누고, 첫 번째 블록부터 층층이 쌓아나가며 지연 시간을 측정했습니다.

신기하게도, 마지막 19번째 블록에서 선형 레이어가 추가돼 FLOPs가 늘었음에도 오히려 지연 시간은 이전 블록까지의 지연 시간인 25.6ms에서 22.3ms로 크게 감소하는 현상을 관측했습니다. 이 실험만으로는 원인을 파악할 수 없어서 추가로 블록을 층층이 쌓아나갈 때 매 블록 마지막에 FC 레이어를 추가해 출력 텐서가 1000 길이의 벡터 형태가 되도록 모델을 설정한 후 실험을 반복했습니다. 그 결과 대부분의 경우에서 레이어를 추가했음에도 많게는 1.5ms까지 지연 시간이 감소하는 모습이 나타났습니다.
이런 현상이 발생하는 원인으로 두 가지 정도를 추측해 볼 수 있었습니다. 첫 번째로 마지막에 선형 레이어를 추가했을 때 CoreML 컨버터가 소프트웨어 관점에서 모델 최적화 과정을 추가로 진행했다고 가정해 볼 수 있습니다. 두 번째로 모델 추론 결과물을 저장해 놓은 버퍼에서 메모리로 데이터를 가져오는 과정에서 시간이 많이 소요된 것이라고 생각해 볼 수 있습니다. 만약 후자라면, 선형 레이어가 없는 18번째 이전 모델들에서는 결과값으로 큰 텐서를 출력하기 위해 많은 양의 데이터를 옮기는 데 걸린 시간이 선형 레이어 하나가 더 들어가면서 추가된 시간보다 훨씬 많았다고 생각해 볼 수 있겠습니다.
마치며
여기까지 뉴럴 네트워크 모델을 모바일 GPU에서 효율적으로 만들기 위한 가설과 실험 내용, 결과를 공유드렸습니다. 간단하게 정리해 보면, TFLite GPU 엔진은 모델의 개별 레이어의 연산 크기가 클수록, 모델의 깊이가 얕아 노드의 숫자가 적을수록 같은 시간에 더 효율적으로 연산을 수행할 수 있습니다. 여기에 합성곱 커널 크기도 되도록 3을 유지하는 게 좋고, 연산 그래프가 여러 가지로 나뉘고 병합되는 구조를 지양해야 더 효율적으로 연산할 수 있습니다. 또한 실험을 통해 자주 사용하는 연산들과 모델을 사용할 때 CoreML이 좋은 성능을 보여주는 것도 확인할 수 있었는데요. 이번 실험에 사용한 네트워크 외에도 다양한 구조의 네트워크 모델이 많이 있고, 그중 어떤 모델들은 TFLite GPU에서 더 좋은 성능을 보여주기도 하기 때문에 항상 CoreML만을 고집할 필요는 없다고 생각합니다.
아직 모바일 기기에 대한 이해가 부족하고 기기와 모델 실행 환경을 완벽하게 통제하지 못했기 때문에 TFLite 모바일 GPU를 잘 사용하기 위한 가이드라인이 이렇다고 확실하게 제시할 수는 없습니다. 이번 실험이 넓은 범위의 연산자들과 수많은 기기를 대표할 수는 없으며, TFLite GPU 사용과 관련해서도 아직 남아 있는 의문이 많은 상태이기도 합니다. 하지만 향후 모바일에 사용할 뉴럴 네트워크를 설계할 때 이번 실험과 결과를 참고한다면 성능 향상에 의미 있는 도움이 될 수 있다고 생각합니다. 아직 Android 기기는 실험해 보지 않았는데요. 이 글에 많은 관심을 보여주시고, 또 Android에 맞춰 모델 개선이 필요한 시점이 온다면 Android 기기도 추가로 실험해서 올려보겠습니다. 긴 글 읽어주셔서 감사합니다.