AWS 기술 블로그
클레온의 AWS Inferentia를 이용한 디지털 휴먼 생성 모델 추론 비용 50% 절감 사례
클레온은 디지털 휴먼을 통한 진정한 소통을 꿈꾸는 스타트업입니다. 지금까지의 소통은 물리적, 시간적, 공간적, 언어적 문제가 있었습니다. 저희는 디지털 휴먼을 활용해 언제 어디서든 쉽고 빠르게 소통할 수 있는 세상을 만들고자 노력합니다.
저희의 서비스는 크게 세 가지 입니다. 1장의 사진과 내 목소리로 디지털 휴먼을 만드는 클론, 다양한 컨셉의 디지털 휴먼과 언제든지 대화하는 챗 아바타, 더빙 언어에 따라 입모양이 바뀌는 클링입니다. 곧 연예인 디지털 휴먼과 대화하는 서비스인 크리챗도 출시 예정이니 많은 관심 부탁 드립니다.
자연스러운 디지털 휴먼을 만들기 위해서는, 얼굴, 목소리, 몸 등 다양한 측면에서 딥러닝 모델을 활용해야 합니다. 이를 위해서는 가벼운 모델을 개발하고 빠른 추론을 할 수 있도록 최적화하는 것이 핵심입니다. 이를 위해 클레온에는 가볍고 성능 좋은 모델을 연구하는 Product AI 팀과 빠른 추론을 할 수 있도록 최적화하는 Inference팀이 있습니다. 이번 글에서는 Product AI 팀과 Inference팀이 협업하여 클레온의 입 모양 생성 모델을 AWS Inferentia를 이용해서 모델 추론 비용을 50% 이상 줄인 경험을 공유하고자 합니다.
추론 서버 비용 절감의 필요성
디지털 휴먼을 만들기 위해서 클레온에서는 생성형 AI를 사용하고 있습니다. ChatGPT 사례에서 알 수 있듯이, 생성형 AI는 모델 추론 비용이 만만치 않습니다. 나아가 저희는 연예인 디지털 휴먼과 대화하는 서비스인 크리챗을 B2C로 제공하려고 합니다. 많은 수의 고객에게 안정적인 서비스를 제공하기 위해서는 다수의 GPU 서버가 필요하며, GPU 서버 비용 절감은 매우 중요한 문제입니다.
저희가 기존에 사용하던 NVIDIA T4 GPU가 4장 장착된 g4dn.12xlarge 인스턴스 1대의 월 사용 비용이 $2,800 달러 수준입니다. 고객 수가 늘어나면 비례하여 GPU 서버 수도 증가하기 때문에, 비즈니스 쪽에서 GPU 서버 비용을 30% 정도 줄여 달라는 요청이 들어왔습니다. 비용 줄일 방법에 대해 고민을 하던 중, 딥 러닝 추론 애플리케이션에 필요한 고성능을 최저 비용으로 제공하는 AWS Inferentia 액셀러레이터에 대해 알게 되었습니다. AWS Inferentia1 의 경우 NIVIDIA T4 GPU와 FP16 FLOPS가 약 64 TFLOPS로 거의 동일 하면서, 가격은 70% 정도 저렴하였습니다. 디지털 휴먼 추론 비용을 절감하기 위해, 저희는 AWS Inferentia1를 클레온의 입모양 생성 모델 추론 서버로 사용 가능한지 테스트해 보았습니다.
AWS Inferentia1에 입모양 생성 모델을 배포하기 위하여 AWS Neuron SDK를 사용하여 BF16 데이터 타입으로 변환 하였으며, 기존에 사용하던 NVIDIA FP16과 비교 하였을 때 결과 품질 차이도 거의 없는 것을 확인하였습니다. 추가적으로 g4dn.xlarge 대비 inf1.xlarge를 사용하면 같은 처리량(frames/sec)에 대해 39.50% 비용을 절감할 수 있는 것을 확인하였으며, g4dn.12xlarge에 비해 inf1.6xlarge를 사용하면 57.90% 비용을 절감할 수 있는 것을 확인하였습니다.
Neuron SDK 통한 모델 컴파일 과정 중 배운 경험
AWS Inferentia 칩에서 딥러닝 모델을 추론하기 위해서는 딥러닝 모델을 AWS Neuron SDK를 통해 컴파일해야 합니다. 컴파일하는 방법은 Neuron SDK 공식 문서에 잘 정리되어 있습니다. 튜토리얼 및 다양한 예제코드가 있어 환경 설정 및 컴파일에 대해 쉽게 익힐 수 있었습니다.
AWS Inferentia 대상으로 학습된 모델을 컴파일 하기 위해서는 모델이 Neuron SDK에서 지원하는 operator들을 사용하는지를 가장 먼저 확인해야 합니다. 다행히도 저희 모델의 모든 operator들이 Neuron SDK에서 지원 되었지만, 실제 컴파일 과정에서 다양한 에러들이 발생하였습니다. 저희가 겪었던 어려움을 공유함으로써, AWS Inferentia를 사용하시려는 분들이 쉽고 빠르게 컴파일 할 수 있기를 바랍니다.
저희가 이번 Neuron 컴파일 과정에서 배운 것은 다음과 같습니다.
- 큰 부분을 작은 부분 여러 개로 분할하여 컴파일 진행
- 특정 함수에 대한 argument 값을 변경해 가면서 컴파일 진행
- torch에 친화적이며 정리된 코드 사용
1. 큰 부분을 작은 부분 여러 개로 분할하여 컴파일 진행
일반적으로 딥러닝 모델의 경우, 자주 사용되는 연산들을 묶어 하나의 블록으로 만들고, 그 블록들을 합쳐 큰 모델을 만듭니다. 저희 모델 또한 PyTorch 기반의 여러 블록들로 구성되어 있습니다. 여러 블록들이 합쳐져 있다 보니 코드 양도 방대하였으며, 이로 인해 컴파일 과정 중 발생하는 에러가 어느 블록에서 어떤 원인으로 발생하는지 알기 어려웠습니다. 저희가 마주쳤던 에러의 종류로는, Error location이 Unknown인 “An Internal Compiler Error”, neuron-cc 컴파일러가 비정상적으로 종료되는 “neuron-cc crashed (SEGFAULT)”, 컴파일 시간이 매우 길어지는 “LARGEST INSTRUCTION COUNTS” 등이 있었습니다. 이러한 에러들의 경우, 로그만으로는 어디서 에러가 발생하는지 알기 어려웠습니다.
[그림 1.] An Internal Compiler Error 발생. Error location: Unknown.
저희는 먼저 Neuron SDK에서 저희의 모든 operator를 지원한다고 하였기에, 위의 에러들을 발생시키는 원인은 저희 코드일 것이라고 생각하였습니다. 나아가 딥러닝 모델 그래프의 블록들은 서로 독립적이기에, 작은 블록들을 먼저 컴파일 한 후에 그것들을 합쳐서 컴파일하는 방법을 사용하기 좋다고 판단하였습니다. 즉, 하나의 블록부터 차근차근 컴파일을 시도하고 추후에 컴파일에 성공한 블록들을 합쳐서 컴파일 하면, 큰 모델도 컴파일 할 수 있을 거라 생각하였습니다.
하나의 블록부터 컴파일을 시도하였으며, 블록 내부에서도 동일하게 더 작은 연산으로 나누어 차례차례 컴파일하는 방법을 사용하였습니다. 상대적으로 컴파일이 어려워 보이는 operator를 간단한 operator로 바꾼 후, 컴파일이 되는지 확인하였습니다. 어려운 operator를 바꿀 때, Tensor slicing, concat, squeeze/unsqueeze, arithmetic operation들을 잘 활용하면, 손쉽게 어떤 operator에서 에러가 발생하는지 알 수 있습니다. 이를 통해 에러를 발생시키는 모든 함수들을 찾았으며, 수정 후 최종적으로 큰 모델이 모두 컴파일 완료되었습니다.
2. 특정 함수에 대한 argument 값을 변경해 가면서 컴파일 진행
에러가 발생하는 함수를 찾던 도중, 저희의 경우 torch.nn.functional.conv2d() 함수에서 에러가 발생하는 것을 발견하였습니다. 위 함수에는 input, weight argument 이외에, bias, stride, padding, dilation, groups와 같은 다양한 옵션 argument들이 사용되었습니다. 위 함수 사용에 있어 어떤 부분이 문제인지 정확히 확인하기 위하여, argument들의 값을 0 또는 1 부터 증가 시키면서 컴파일 해 보았습니다. argument 값들이 0 또는 1의 기본 값일 때는 컴파일이 잘 되었습니다. 그러나 저희의 사용 사례인, groups 파라미터 값이 2 이상인 경우에 컴파일이 되지 않는 것을 확인하였습니다.
기존 에러 발생 코드는 다음과 같았습니다.
f = torch.cat([f]*num_channels, dim=0)
x = torch.nn.functional.conv2d(input=x, weight=f, groups=num_channels)
저희 모델의 경우, conv2d() 함수를 사용할 때, groups 값을 주어 input channel 별로 다른 weight 값으로 convolution을 진행하는 depth-wise convolution을 진행합니다. 이때, groups 파라미터 값이 512 이상인 경우, “neuron-cc crashed (SEGFAULT)” 에러가 발생하는 것을 확인 하였습니다. torch.chunk()로 분할하여 groups 파라미터 값이 512 미만이 되게 만들어 convolution을 진행하였더니 문제 없이 컴파일 되는 것을 확인하였습니다.
다음은 torch.chunk()
로 분할하여 위 에러를 해결한 코드 입니다.
neuron_max_group_size = 512
if num_channels >= neuron_max_group_size:
chunk_num = num_channels // neuron_max_group_size
x_splits = torch.chunk(x, chunk_num, dim=1)
f = torch.cat([f]*neuron_max_group_size, dim=0)
conv_results = []
for x_split in x_splits:
conv_result = torch.nn.functional.conv2d(input=x_split,
weight=f,
groups=neuron_max_group_size)
conv_results.append(conv_result)
x = torch.cat(conv_results, dim=1)
else:
f = torch.cat([f]*num_channels, dim=0)
x = torch.nn.functional.conv2d(input=x, weight=f, groups=num_channels)
또한, groups 파라미터 값이 2 이상인 경우에 큰 input channel 값과 추가적인 산술 연산이 있으면, LARGEST INSTRUCTION COUNTS warning이 발생하고 컴파일 시간이 매우 길어지는 것을 확인 하였습니다. groups 파라미터 값이 1인 경우에는, input channel 값이 2048로 커져도 컴파일이 잘 진행되었습니다. 그러나 groups 파라미터 값이 2인 경우에는 input channel 값이 256 이상, groups 파라미터 값이 8인 경우에는 input channel 값이 128 이상인 경우에 “neuron-cc crashed (SEGFAULT)” 에러가 발생하였습니다. 이 부분 또한 torch.chunk()로 분할하여 convolution을 진행하였더니 문제 없이 컴파일 되었습니다.
위에서 발생한 두 가지 에러 모두 재현 가능한 형태로 aws-neuron-sdk github에 issue로 올렸으며, 담당자 분들이 추후 해결해 주시기로 하였습니다 (이슈 링크 1, 이슈 링크 2)
3. torch에 친화적이며 정리된 코드 사용
처음 컴파일을 시도하였을 때, 96코어, 180GB의 RAM이 있는 EC2 인스턴스에서 진행하였습니다. 이때, RAM 부족으로 인해 서버가 다운되는 현상이 반복되었습니다. Neuron SDK에서 컴파일을 실행하는 torch_neuron.trace()라는 함수의 동작 방식을 좀 더 이해하기 위해 document를 참고한 결과, torch.jit.trace() 함수와 유사하게 사용되는 것을 알게 되었습니다. 추가적으로, torch.jit.trace() 문서에 다음과 같이 tracing은 tensor 형태의 데이터 구조와 친화적이라고 적혀있었습니다.
Tracing is ideal for code that operates only on Tensors and lists, dictionaries, and tuples of Tensors.
저희 코드에서는 numpy와 torch.Tensor를 혼용해서 사용하기도 하고, numpy 쪽에서 자주 사용되지 않는 API들을 사용하기도 하였습니다. 이러한 부분으로 인해 torch_neuron.trace() 함수가 정상적으로 동작하지 않는다고 생각하였으며, torch_neuron.trace() 함수에 친화적이도록 numpy 사용을 지양하고, list나 tuple 등의 데이터 구조도 복잡하지 않게 사용하도록 구현하였습니다.
정리된 코드를 사용하는 것 또한 중요합니다. 사용하지 않는 텐서(torch.Tensor)가 코드에 있는 경우, “An Internal Compile Error”가 발생하는 것을 확인하였습니다 (이슈 링크). 이러한 텐서들을 삭제하였더니 에러가 없어졌습니다. 또한 None 값을 반환하는 함수 등도 삭제하였습니다. 결과적으로 12코어, 32GB RAM의 데스크탑에서 전체 모델 컴파일이 성공적으로 되는 것을 확인하였습니다.
AWS Inferentia 도입 결과
기존에 사용하던 g4dn 인스턴스와 AWS Inferentia1 인스턴스를 비교하기 위해, NVIDIA Triton server 추론 서버를 사용하였습니다. AWS Inferentia에서 triton server를 사용하는 방법은 triton-inference-server github의 python_backend/inferentia에 문서화가 잘 되어있습니다. 모델 성능 테스트는 Triton Performance Analyzer를 사용했습니다.
저희는 성능 비교를 위해 g4dn.xlarge 인스턴스와 inf1.xlarge 인스턴스를 사용했습니다. g4dn.xlarge 인스턴스는 NVIDIA T4 GPU가 1개 장착되어 있으며, 65 FP16 TFLOPS 성능을 보여줍니다. inf1.xlarge 인스턴스에는 AWS Inferential1칩이 한 개 장착되어 있으며, AWS Inferentia1 칩은 4개의 Neuron core로 구성되어 있고, 총 64 FP16/BF16 TFLOPS 성능을 보여줍니다. g4dn 인스턴스에 올라간 모델은 NVIDIA TensorRT 8.6.1.6버전을 사용하여 fp16으로 컴파일하였으며, AWS Inferentia1 인스턴스에 올라간 모델은 bf16으로 컴파일 하였습니다.
[그림 2.] g4dn.xlarge와 inf1.xlarge의 배치 크기(batch size)에 따른 throughput과 latency 그래프
인스턴스 타입별로 최적의 추론 성능을 낼 수 있는 배치 크기가 다르다.
또한, 동일한 인스턴스를 사용하더라도 모델 추론 시 사용하는 배치(batch) 크기에 따라서 추론 성능이 달라질 수 있습니다. 그래서, 저희는 인스턴스 별 배치 크기에 따른 throughput과 latency 값을 측정하였습니다. Throughput의 경우 초당 처리하는 총 프레임 개수로 측정하였습니다. 즉, 1초에 배치 크기가 4인 요청을 4개 처리한다면, throughput은 16 frames/s가 됩니다. Latency는 한 번의 요청이 완료되어 응답을 받기까지의 시간을 측정하였습니다.
g4dn.xlarge의 경우, 배치 크기 4까지는 배치 크기에 비례하여 throughput이 증가하지만, 그 이후로는 어느 정도 수렴하는 것을 볼 수 있습니다. Latency는 배치 크기에 비례하는 값을 보여 주었습니다. inf1.xlarge의 경우, 배치 크기 16까지 배치 크기에 비례하여 throughput이 증가하였으며, latency 또한 배치 크기 16까지는 어느 정도 일정한 값을 보여 주었습니다. 배치 크기 32에서는 latency가 비례하여 커지는 것을 보여 주었습니다.
실험 결과 그래프를 통해서 볼 수 있는 한 가지 재미있는 사실은, inf1.xlarge와 g4dn.xlarge의 throughput이 수렴하는 순간의 배치 크기가 다르다는 점입니다. AWS Inferentia1 칩은 4개의 Neuron core로 이루어져 있으며, AWS Inferentia1 칩에 들어오는 batch를 4로 나누어서 각각의 Neuron core에 나누어 줍니다. 즉, batch가 4인 경우에 실제로 각각의 Neuron core들이 받는 batch 크기는 1이 됩니다. 이러한 AWS Inferentia1 칩의 특성으로 인해, batch 16까지 throughput이 증가하는 것을 확인할 수 있었습니다.
다음으로는 저희의 목표 throughput을 달성하기 위해 각 인스턴스를 사용하였을 때 발생하는 총 비용을 산정해 보았습니다.
[표 1.] g4dn과 inf1 인스턴스 성능 대비 비용 비교 분석 결과
동일한 성능을 얻기 위해서 inf1.xlarge 사용 시, g4dn.xlarge 보다 비용이 39.2%(1) 줄어들고,
g4dn.12xlarge 보다 inf1.6xlarge를 사용 시 비용을 59.78%(2) 절감할 수 있다.
두 인스턴스의 성능 대비 비용을 정확하게 비교 하기 위해서 g4dn과 inf1 인스턴스 타입별로 throughput과 latency를 최적화 할 수 있는 환경에서 실험을 진행했습니다. 그래서, g4dn.xlarge는 batch 크기를 4로 설정하고, inf1.xlrage는 batch 크기를 16으로 설정하였습니다.
표 1의 실험 결과를 보면, 목표 throughput을 1500 frames/s로 설정한 경우, 목표 throughput을 달성하기 위해서 필요한 g4dn.xlarge 인스턴스는 1500 / 153.24 = 9.79 로서, 총 10대의 인스턴스가 필요합니다. inf1.xlarge 인스턴스는 1500 / 115.04 = 13.04 로서, 총 14대의 인스턴스가 필요합니다. 필요한 인스턴스의 개수에 인스턴스 별 시간당 비용을 곱하면, 목표 throughput 달성을 위한 총 비용이 계산됩니다. 10 x g4dn.xlarge의 비용은 5.26 $/hr 이며, 14 x inf1.xlarge 비용은 3.192 $/hr 입니다. 결과적으로 목표 throughput 달성을 위한 총 비용이 inf1.lxarge가 g4dn.xlarge에 비해 39.32% 절감되는 것을 확인할 수 있습니다. 4개의 GPU가 있는 g4dn.12xlarge와 inf1.6xlarge를 비교하였을 때는, 59.78% 절감되는 것을 확인할 수 있습니다.
정확한 비용 절감 비율은 각 상황의 목표 throughput에 따라 달라질 수 있습니다. 하지만, 대략적인 비용 절감 비율은 인스턴스 사용 비용과 최적 배치 크기에 따른 throughput을 곱한 값으로 비교할 수 있습니다. 이에 따라 결과적으로 g4dn.xlarge 대비 inf1.xlarge를 사용하면 같은 처리량에 대해 39.50% 비용을 절감할 수 있으며, g4dn.12xlarge에 비해 inf1.6xlarge를 사용하면 57.90% 비용을 절감할 수 있습니다.
마무리
딥러닝 서비스의 성공을 위해서 GPU 서버 비용을 줄이는 것은 매우 중요합니다. 클레온의 딥러닝 모델을 AWS Inferentia에 올려보는 경험을 통해, 앞으로 GPU 서버 비용을 50% 이상 줄일 수 있을 것 같습니다. 또한 이번에 배운 Neuron SDK 컴파일 경험을 활용한다면, 추후에 새로운 모델이 개발 되었을 때도 쉽게 AWS Inferentia에 배포할 수 있을 것입니다. 앞으로도 클레온에서는 새롭게 개발한 모델들의 추론 서버로 AWS Inferentia를 적극적으로 사용할 계획입니다. 또한, AWS Inferentia를 이용해서 추론 비용을 절약함으로써 고객들에게 보다 합리적인 가격으로 디지털 휴먼 서비스를 제공하기 위해서 노력할 것입니다.