Product
이전 게시물에서 말씀드린 것처럼 Optimium은 AI 개발자들을 위한 강력한 추론 엔진으로, 최적의 AI 모델을 쉽고 빠르게 배포할 수 있도록 지원합니다. 아시다시피 Optimium은 사용자가 빠른 추론 속도를 느낄 수 있게 다양한 방법들을 사용하고 있습니다. 오늘은 AI 모델의 전체 계산을 최적화하고 빠르게 하는 Operator Fusion에 대해 자세히 설명하겠습니다.
Sewoong Moh
2024년 4월 22일
안녕하세요. AI 추론 최적화 엔진을 만들고 있는 Optimium 팀의 모세웅(Sewoong Moh)입니다. 이전 게시물에서 말씀드린 것처럼 Optimium은 AI 개발자들을 위한 강력한 추론 엔진으로, 최적의 AI 모델을 쉽고 빠르게 배포할 수 있도록 지원합니다. 아시다시피 Optimium은 사용자가 빠른 추론 속도를 느낄 수 있게 다양한 방법들을 사용하고 있습니다. 오늘은 AI 모델의 전체 계산을 최적화하고 빠르게 하는 Operator Fusion에 대해 자세히 설명하겠습니다.
Operator Fusion 이란?
Operator Fusion은 서로 다른 Layer(또는 Operator)를 하나의 Layer로 융합하는 것을 의미합니다. 서로 다른 Layer의 연산을 하나의 Layer로 융합할 수 있으면 얻을 수 있는 이점이 무엇이 있는지 예시를 통해 확인할 수 있습니다. 다음은 Mul
이후에 Add
연산이 진행되는 경우의 예시입니다.
위와 같이 Fusion이 진행되지 않은 경우에는 각 Tensor(a,b,c
)의 Shape이 n*n*n*n
이라고 할 때, 총 2*n⁴
만큼의 Loop를 돌며 연산이 진행됩니다. 또한 Input과 Output에 대한 접근은 2*(2*n⁴+n^4)=6*n⁴
만큼 일어납니다. Mul
과 Add
가 Fusion이 된다면 어떻게 될까요?
Add
연산에 있던 Loop가 사라지고, Mul
연산의 Loop 내에서 두 가지 연산이 함께 연산 되기 때문에 n⁴
만큼의 Loop만 돌게 됩니다. 이를 통해 For Loop 관련하여 약 2배 정도의 성능 향상을 기대할 수 있습니다. 그리고 Input과 Output에 대한 접근은 3*n⁴+n⁴=4*n⁴
으로 감소(~30%)하여 추가적인 성능 향상을 기대할 수 있으며, 이미 레지스터 또는 캐시에 올려진 값을 사용하여 연산을 하기에 더 빠르게 데이터에 접근할 수 있습니다. 또한, a*b+c
의 연산 패턴은 Compiler에서 FMA Instruction 을 사용하여 최적화 가능성을 제공하여 추가적인 속도 향상도 기대할 수 있습니다. 이러한 예시를 통해 알 수 있는 Operator Fusion의 장점은 다음과 같습니다.
Loop 횟수 감소
Memory 접근 횟수 감소
추가적인 최적화 기회 제공
💡 <FMA Instruction>
· a * b + c 라는 연산을 곱셈 1번, 덧셈 1번 총 2번의 계산을 수행하지 않고 한 번에 계산할 수 있도록 프로세서 아키텍쳐가 제공하는 명령어입니다.
· 연산 횟수를 줄일 수 있으므로 속도도 빨라지고 반올림(rounding) 횟수를 줄일 수 있으므로 계산 정확도도 높아집니다.
· 또한 대부분의 아키텍쳐에서 FMA에 대한 SIMD 명령어도 제공하기 때문에 이를 활용해야 최대의 성능을 누릴 수 있습니다.
SIMD 명령어가 궁금하시면 아래의 지난 게시물을 확인 부탁드립니다.
Fusion의 어려움
Operator Fusion을 이용하면 얻을 수 있는 장점은 확실합니다. 따라서 TFLite, XNNPack, OpenVINO와 같은 다른 추론 엔진들도 Operation Fusion을 이미 활용하고 있습니다. 하지만 이들은 Metaprogramming을 지원하는 Optimium과 달리 모든 Operator 소스 코드를 직접 구현해야 하는 근본적인 특징으로 인해 매우 제한적인 Operator Fusion밖에 수행하지 못합니다. 실례로 XNNPack의 경우 Convolution + ReLU, ReLU6와 같은 Clipping 계열의 Activation만을 지원합니다. 이렇게 제한적일 수밖에 없는 이유를 구체적으로 아래에서 설명드리겠습니다.
💡 <시간없는 분들을 위한 “Fusion의 어려움” 2줄 요약>
1. Fusion 된 Operator를 구현하는 방법으로는 수많은 Fusion 조합을 대응하기 불가능하고 유지 보수성이 떨어져서 관리가 어려움
2. Runtime 상황에서 비효율적인 Fusion 수행
Fusion은 여러 가지 연산의 조합으로 이루어질 수 있습니다. 이러한 조합은 모델의 Layer 구성에서 자주 등장하는 Layer 패턴에 따라 달라집니다. 가장 많은 형태는 Convolution, Fully-Connected Layer 이후에 Activation이 Fusion 되는 경우입니다. 이 외에는 Element-wise Binary Operations(Add, Mul, Sub, Div, …) 등이 연속적으로 나타나는 경우가 자주 존재합니다.
Fusion에 사용되는 연산의 종류뿐만 아니라 갯수와 순서도 문제입니다. Mul
과 Add
의 예시에서처럼 2개의 연산만 Fusion이 가능한 것은 아닙니다. 아래의 예시처럼 Fusion 되는 연산의 갯수가 3개 또는 4개가 될 수도 있습니다.
또한, 연산의 갯수 뿐만 아니라, 같은 연산의 조합에서도 연산의 순서가 서로 다를 수 있습니다.
이처럼 다양한 연산의 종류와 조합 및 순서 등을 고려하여 가능한 모든 경우의 수에 대해서 코드를 사전에 작성해놓는 것은 불가능합니다. 따라서 대부분 추론 엔진들은 Fusion의 종류를 제한하여 몇 가지 조합에 대해서만 Fusion을 허용하고 있습니다.
두 번째로 다양한 연산 조합에 대해서 Fusion을 구현하기도 어렵지만, 이렇게 구현된 코드들을 관리하기도 어렵습니다. Fusion은 다양한 연산 조합들로 이루어져 있기 때문에 반복 사용되는 Layer의 구현들이 여러 Fused Layer에 걸쳐서 존재하게 됩니다. (아래 이미지에서 Add
) 이 때 만일 구현 했던 Add
연산에서 문제가 발견되어 수정하고자 한다면, 조합별로 구현된 Layer 중에서 Add
가 들어가는 경우를 찾아서 각각에 대하여 하나하나 수정을 해야 합니다. 이러한 문제는 향후 연산의 안정성을 떨어뜨리는 요인이 될 수 있으며, 패키지 관리도 어렵게 할 수 있습니다.
위와 같이 다양한 조합의 Fusion Layer의 작성을 회피하기 위해서 Runtime 상황(Layer가 연산을 수행하는 상황)에서 동적으로 Fusion을 수행하는 방법이 있지만, 여기에는 속도 저하 요소가 존재합니다. Conv2D Layer에서 Fusion을 수행하는 예시를 보겠습니다. Layer가 실행되는 도중에 Conv2D 이후에 Fusion 되는 Layer를 조건문(If)을 통해 확인하여 Layer의 종류에 따라 적절한 연산을 수행해주면 됩니다. 여기서 Fusion 되는 Layer가 한 개가 아니고 여러 개 존재한다면, 반복문(For)을 돌면서 Layer의 종류를 확인하고 이에 맞는 연산을 수행해야 합니다. 그러나 Runtime 상황에서 조건문과 반복문의 조합은 속도 면에서 굉장히 비효율적이며 비싼 연산입니다. 이런 형태로 Fusion을 수행하면 Fusion으로 얻는 속도적인 이득보다, 비효율적인 조건문과 반복문 수행으로 잃는 부분이 많아질 수밖에 없습니다.
Fusion과 Nadya
Optimium에서는 위의 한계점을 극복하고 최대 성능을 달성하기 위해서 Metaprogramming을 지원하는 사내 자체 개발된 언어인 Nadya를 활용하여 Layer를 구현하고 있습니다. 과정을 간략하게 나타내면 다음과 같습니다.
Code Geneartion
단계에서 모델의 정보 중 Layer의 정보를 각 Layer의 Code를 생성하는 Nadya 모듈로 전달하게 됩니다. 각 Nadya 모듈은 이 정보를 바탕으로 Runtime에서 활용될 수 있는 최적화 된 Code들을 제공합니다. 이렇게 Runtime Code를 Code Generation을 활용하여 생성하는 것을 Metaprogramming이라고 합니다.
※ 자세한 내용은 아래의 지난 게시물에서 확인할 수 있습니다.
Optimium에서는 이러한 Metaprogramming 방법을 활용하여 기존의 제한된 Fusion 기능을 보다 유연하게 사용할 수 있도록 Fusion Module을 개발하였습니다. 이를 통해 앞서 살펴보았던 Fusion의 어려움을 극복하고자 합니다. 큰 틀에서 Optimium의 Fusion은 다음과 같은 목표를 가지고 있습니다.
1. Fusion된 Operator를 구현하는 방법으로는 수많은 Fusion 조합을 대응하기 불가능
→ Metaprogramming을 통한 무제한에 가까운 Fusion Layer지원
2. Runtime에서 비효율적인 Fusion 수행
→ Code Generation 상황에서 Fusion을 수행하여 Runtime에서 효율적인 연산 수행
Fusion의 형태는 바라보는 관점에 따라서 여러 가지로 나눌 수 있습니다. 우선 Layer들이 연결된 Graph 상에서 Layer의 상대적 위치에 따른 분류입니다. Add-Conv-Mul-ReLU
와 같이 Fusion이 일어나면, Conv
를 기준으로 Add
는 Conv
보다 앞에 있는 Pre-Fusion
, Mul-ReLU
는 Conv
보다 뒤에 위치하는 Post-Fusion
으로 나눌 수 있습니다.
Optimium에서는 Code generation 시기에 위와 같이 구분된 Fusion의 정보를 Layer로 전달하고, Layer에서는 Fusion Module로 전달합니다. 이 정보를 기반으로 Fusion Module에서는 Fusion 된 Layer의 Runtime Code를 생성하는 시기에 Fusion 관련 Code를 생성할 수 있는 함수를 제공합니다. Fusion 된 Layer에서는 이 함수들을 정해진 위치에서 호출하는 것만으로 Fusion 종류, 갯수, 형태와 상관없이 적절한 Fusion Code를 생성할 수 있습니다.
Mul-Add-ReLU
의 예시를 살펴보도록 하겠습니다. Optimium의 Graph Module에서는 Mul
Layer의 Code Generation 하는 곳으로 Mul
Layer의 정보와 Fusion 되는 Add-ReLU
에 대한 정보도 전달합니다. Mul
Layer는 Code generation 과정에서 Fusion 정보를 Fusion Module로 전달 합니다. Fusion Module에서는 이 정보를 바탕으로 생성된 Fused Layer의 Code를 Mul
Layer로 전달하면, Mul
Layer에서는 Code를 생성할 때 Fused Layer의 Code를 통합합니다. 이를 통해 Mul-Add-ReL
가 Fusion된 효율적인 Runtime Code를 생성할 수 있습니다.
위 과정을 좀 더 일반화하면, Graph Module에서 임의의 Layer 조합에 대하여 Fusion 정보를 생성할 수 있습니다. 이 정보를 바탕으로 각 Layer의 Code를 생성할 때 Fusion Module에서 생성한 Fused Layer의 Code를 통합함으로써 다양한 Layer, 다양한 Fusion 조합에 효율적으로 대응할 수 있게 됩니다. 여기에서 Fused Layer에 대한 구현은 Layer마다 진행될 필요 없이, Fusion Module에만 구현하면 되기 때문에, 추후에 수정할 사항이 생기더라도 Fusion Module에 구현된 부분만 수정해주면 됩니다. 이를 통해 Fusion의 첫 번째 문제였던 “1.Fusion된 Operator를 구현하는 방법으로는 수많은 Fusion 조합에 대응하기 불가능하고 유지 보수성이 떨어져서 관리가 어려움”가 해결됩니다. 또한, Fusion Module은 Runtime에서가 아니라, Code Generation 상황에서 Fusion된 Code를 생성합니다. 덕분에 실제 Runtime Code는 비효율적인 조건문 없이 연산 Code만 남게 되어 효율적으로 수행될 수 있습니다. 이를 통해 두 번째 문제였던 “2 Runtime 상황에서 비효율적인 Fusion 수행”이 해결됩니다.
Why Optimium?
이.렇.듯! Optimium은 자체 언어 Nadya를 활용하여 Metaprogramming의 강점을 이용하여 기존 Fusion의 약점을 철저히 극복할 수 있습니다. 덕분에 기존 추론 엔진보다 넓은 범위의 Fusion을 지원하면서도, 효율적인 Runtime 연산 성능을 제공합니다.
위 그래프는 Optimium에서 Fusion을 적용했을 때와 적용하지 않았을 때의 성능 차이를 나타냅니다. 보시다시피 Fusion을 통해 약 1.3배 가속이 됨을 확인할 수 있습니다. Convolution+ReLU와 같이 다른 추론 엔진들도 지원하는 Fusion뿐만 아니라 Binary Operator, Padding 등 다양한 연산을 3~4개까지도 Fusion할 수 있는 Optimium이기에 가능한 수치입니다. 위 그래프에서는 Raspberry Pi5에서 측정하였지만 다른 Device에서도 비슷한 개선을 얻을 수 있습니다.
Run Optimium , run!🏃
Optimium은 현재 베타 테스트 진행 중이며, 다양한 하드웨어 환경에 대하여 기존 추론 최적화 엔진들 대비 우수한 성능을 보여 다양한 업체들의 관심을 받고 있습니다. 오늘 설명해 드린 Operator Fusion뿐만 아니라 매우 다양한 최적화 기법들의 집합체로써 개발된 Optimium은 이미 아래와 같이, 많이 사용되고 있는 TensorFlow Lite XNNPACK 대비 우월한 추론 속도를 도출하고 있으니, 직접 경험해보고 싶으신 분들은 아래 베타 신청 링크를 통해 언제든지 연락 부탁드립니다 🙂
👉 https://wft8y29gq1z.typeform.com/to/Sv9In4SI
Life is too short, you need Optimium