brunch

You can make anything
by writing

C.S.Lewis

by 매스프레소 Feb 02. 2020

안드로이드 데이터 처리 파이프라인 최적화

실시간 모바일 딥러닝을 위한 필수 작업

딥러닝 모델을 모바일 환경에서 실행시키기 위해서는 딥러닝 모델 경량화 뿐 만 아니라 모바일 데이터 처리 파이프라인의 최적화도 필요합니다. 실시간 모바일 딥러닝을 구현하기 위해서는 데이터 전처리 시간 또한 무시할 수 없기 때문에 더 좋은 성능을 내기 위해서는 모바일 데이터 처리 파이프라인의 최적화가 필수적입니다. 이번 블로그에서는 안드로이드 스마트폰에서 딥러닝 모델을 실행시키기 위한 데이터 처리 파이프라인 분석과 최적화 작업후기를 공유하고자 합니다.





Data Processing Pipeline in Android


구글에서 제공하는 Tensorflow Lite 샘플앱의 데이터 처리 파이프라인은 아래의 그림과 같습니다.



안드로이드 Camera2 API에서 OnImageAvailable이라는 이벤트 리스너에서 이미지를 인자로 넘겨받습니다. 이를 통해 YUV ByteArray를 만들고 이렇게 만들어진 YUV ByteArray는 시각화 및 딥러닝 모델 입력을 위해 RGB format으로 바뀌는데 YUV2ARGB 로직을 사용해 이를 변경합니다. 그리고 이를 시각화하기 위해 비트맵형태로 바꾼 뒤, 0–255범위의 unsigned int8 값을 -1 ~ 1 사이의 float32로 값으로 표준화(normalizeInput) 시키고 TF lite 모델에 전달하기 위해 BytesBuffer 형태로 만들어 준 후 딥러닝 모델에 전달합니다.


이 중에서 가장 병목이 되는 과정은 convertYUVtoARGB, normalizeInput 과정입니다. 픽셀 하나하나 특정한 연산을 통해 값을 변환해야하기 때문에 다른 과정에 비해 시간이 많이 소요되는 과정입니다. 아래의 프로파일링 결과에서도 보실 수 있듯이, 앞 서 언급한 두 과정이 다른 과정에 비해 월등히 큰 실행 시간을 차지하는 것을 알 수 있습니다.


모든 실험은 Galaxy S10e 에서 실행되었습니다.



프로파일링 결과 코틀린으로 구현된 구글 샘플앱에서 데이터 전처리에만 약 120ms가 소요되는데, 이는 전 처리 과정만 진행해도 최대프레임이 약 8FPS 밖에 안된다는 것을 알 수 있습니다. 따라서 실시간 딥러닝의 성능을 올리기 위해서는 딥러닝 모델 뿐 만 아니라 데이터 처리 파이프라인도 최적화해야함을 알 수 있습니다.

본 블로그에서는 분량 상의 이유로 더 큰 시간이 소요되었던 normalizeInput 과정에 대한 최적화만 상세하게 다루도록 하겠습니다. convertYUVtoARGB도 비슷한 과정을 거쳐서 진행할 수 있기에 큰 문제가 되지 않을 것이라 생각됩니다.



Data Processing Optimization step1 : JNI(Java Native Interface)


가장 직관적인 방법은 Java Native Interface(JNI)를 통하여 최적화하는 것입니다. JNI는 JVM위의 코드가 네이티브 응용 프로그램, C/C++, 어셈블리어 같은 다른 언어들로 작성된 프로그램을 호출할 수 있게하는 프레임워크입니다. 코틀린으로 작성된 샘플앱은 JVM위에서 작동되기에 호환성이 뛰어나나 항상 최선의 성능을 내지는 못합니다. 따라서 병목이 되는 부분을 JNI를 통해 네이티브 언어인 C++로 구현하면 더 빨라질 수 있을것이라 추정했습니다.


initInputArray.kt

https://gist.githubusercontent.com/wlwkgus/81b413c51074b4db2d50994f2f8f8f5b/raw/a35efc2bb02d4293664f0a7e9179cc2b8cfa6e31/initInputArray.kt


모든 픽셀을 순회하면서 정규화 연산을 실행하는 로직을 아래와 같이 JNI를 이용해 변경하였습니다.

initInputArrayJNI.kt

https://gist.githubusercontent.com/wlwkgus/c91011f8e34dcb6a198fdbb4f40c9527/raw/e1af068f9bd4a924768f8c9b84e1306cf24a53bf/initInputArrayJNI.kt


nativelib.cpp

https://gist.githubusercontent.com/wlwkgus/f43e58be03e017f3c38d7233dd21be3b/raw/fa53068dc75f1d7b905db3b00fb56c8f9a460efd/nativelib.cpp


이렇게 최적화한 경우 기존의 평균 64.5ms의 실행시간에서 1.30ms로 약 98%의 실행시간을 단축시킬 수 있었습니다.




Data Processing Optimization step2 : ARM NEON intrinsics


한 개의 명령어로 여러 개의 값을 동시에 처리하는 병렬 프로세서 기술을 SIMD(Single Instruction Multiple Data)라고 합니다. 특히 ARM 사에서 개발한 병렬 프로세서인 SIMD를 NEON 기술이라 하는데 이를 통해 성능을 끌어올릴 수 있습니다. 직접 NEON instruction set을 이용하면 메모리 액세스를 최소화하고 처리량 성능을 향상시켜 데이터 처리 성능을 끌어올릴 수 있습니다. 다만 NEON을 적용하기 위해서는 어셈블리어로 작성해야 하는 단점이 있는데 ARM사에서는 컴파일러가 적절한 NEON 인스트럭션으로 바꿔주는 C함수 셋을 제공하며 이를 NEON intrinsics라고 부릅니다. NEON intrinsics는 항상 최고의 성능을 내지는 못하지만, 생산성 측면에서 유리하기에 이번 최적화 과정에서 적용하기로 결정하였습니다.


ARM NEON Intrinsics 명령어들 조합으로 8bit uint를 32bit float으로 변환하는 과정에서 메모리 액세스를 최적화할 수 없다고 판단해 데이터 로딩 부분만 ARM NEON Intrinsics을 적용하였습니다. vld1q_u32 명령어를 통해 128bit(uint32_t 4개)씩 레지스터에 로딩하면 더 빠른 연산이 가능할 것이라 생각했습니다.


nativelibNEON.cpp

https://gist.githubusercontent.com/wlwkgus/e1929c8c85d7f6c950876fa4bc7aa81c/raw/a3c4fa3a04908cc1c8e72da4097d4b765a95aecc/nativelibNEON.cpp


그러나 실제로는 기존 C++ 구현보다 약간 느려졌는데, 이는 레지스터 로딩을 통해 메모리 액세스를 효율화하는 것 보다 NEON intrinsics가 만드는 오버헤드가 더 컸기 때문에 이러한 결과가 발생했을 것이라 추정했습니다. 이를 통해 최적화를 진행하기 위해서는 ARM NEON Instruction set을 통해 더 진행해야 한다고 결론내릴 수 있습니다.





Conclusion


이번 블로그를 통해 안드로이드 딥러닝 데이터 처리 파이프라인에 대해 살펴보고 제가 진행한 최적화 과정을 소개하였습니다. JNI와 NEON intrinsics가 익숙하지 않은 사람들을 위해 샘플 코드를 블로그 내용중에 추가하였습니다. 이러한 데이터 처리 파이프라인 최적화는 딥러닝 연구자, 안드로이드 개발자 모두에게 생소한 분야이기 때문에 관련 자료를 찾기가 쉽지 않았습니다. 이 블로그가 실시간 모바일 딥러닝 관련 프로젝트에 관심있으신 분들에게 큰 도움이 되었으면 좋겠습니다.


추가적으로 궁금하신 점이 있으시면 부담없이 tony.kim@mathpresso.com로 연락주세요.

브런치는 최신 브라우저에 최적화 되어있습니다. IE chrome safari