Skip to main content

Bert의 확장 – 이제 CPU에서 하루 10억 건 이상의 요청을 처리할 수 있습니다

8월

03, 2020

by coberst


테크

다음은 데이터 과학자와 기계학습 엔지니어들에게 주어진, 전형적인 ‘닭이 먼저냐 계란이 먼저냐’의 문제입니다. 사업 개발을 위한 새로운 기계학습 모델 개발 시, 먼저 정확한 제품 개발에 치중한 다음 프로덕션의 속도를 높일 것인가? 아니면 속도를 우선시한 다음 정확성 향상 작업에 착수할 것인가? 이 질문에 대한 토론의 장은 언제나 뜨겁게 달아오르곤 합니다.

2019년 초, Roblox에서는 최첨단 Bert 딥 러닝 모델을 도입해 Roblox용 차세대 텍스트 분류기를 개발하기 시작하면서 이러한 상황에 직면하게 되었습니다. 엔트리 레벨 기본 모델이 1억 1천만 개의 매개 변수로 시작하는 대규모 심층 신경망인 Bert 모델이 프로덕션 과정에서 요구되는 속도 및 처리량 요구 사항을 충족할 수 있는지 확신할 수가 없었기 때문입니다. 논의 끝에 우리는 먼저 Bert 기반 텍스트 분류기의 정확도에 먼저 초점을 맞추기로 결정했으며, 그 후 몇 개월 만에 다행히도 적절한 프로덕션을 위한 속도 또한 확보할 수 있었습니다. 따라서 우리에겐 ‘닭(정확성)’이 먼저였고, ‘계란(속도)’이 나중이었다 할 수 있습니다. 이것은 상당히 어려운 결정이었습니다. 이제 우리는 이와 흡사한 상황을 겪고 있는 이들을 위해, 이제부터 Bert 추론의 속도를 최적화할 수 있는 방안을 공유하려고 합니다. 즉 계란(특정 Bert 모델의 프로덕션 속도를 높이는 전략)에 먼저, 그리고 닭(Bert 모델의 정확성 향상)에 집중하는 것입니다.

프로덕션 시 Bert의 속도를 높이기 위한 여정

자연어 처리(NLP)에 대한 Bert의 위력은 2019년에 널리 보고되었고, Roblox에서도 실제로 데이터를 처리하며 이를 몸소 체험한 바 있습니다. Bert 딥 러닝 모델을 미세 조정함으로써 텍스트 분류 및 개체명 인식(NER) 응용 프로그램을 근본적으로 탈바꿈했으며, 모델 성능(F1 점수)은 이전 모델에 비해 10% 이상 개선되었습니다.

Bert 모델 개발 과정은 방대하고 유용한 리소스최고 수준의 라이브러리로 인해 가속화할 수 있었지만, 지연 시간이 짧고 처리량이 높은 프로덕션 사용 사례에 대한 PyTorch에서의 Bert 확장 방법에 대해서는 다양한 리소스를 찾지 못했습니다. 또한 많은 NLP 서비스는 20ms 미만의 대기 시간에 초당 25,000개 이상의 추론(즉 하루에 10억 개 이상의 추론)을 처리해야 합니다.

이 글의 목적은 Roblox 텍스트 분류에서 Bert의 추론 속도를 30배 이상 가속화한 과정을 공유하는 것입니다. 이러한 최적화 작업(아래 요약)을 통해 Bert의 속도를 사용자가 만족할 만한 수준으로 향상할 수 있었을 뿐 아니라, CPU에서 합리적인 비용으로 프로덕션을 실행할 수 있을 만큼 경제적인 도구를 제공할 수 있었기 때문입니다.

벤치마크

이러한 목적을 달성하기 위해, Bert를 미세 조정하여 개발한 텍스트 분류 서비스의 추론 속도 향상에 중점을 두고 내용을 전달하도록 하겠습니다. 여기서 ‘추론’이란 텍스트를 입력으로 가져와 입력 텐서로 토큰화하고 해당 텐서를 Bert 기반 모델에 공급하며 모델에서 생성된 분류 레이블을 반환하는 과정을 의미합니다.

Glass Bridge in China

위 중국의 고공 유리 다리 그림은 대기 시간 및 처리량에 대한 좋은 비유입니다. 대기 시간은 한 사람이 다리를 건너는 데 걸리는 시간에, 처리량은 특정한 시간 내에 다리를 건너는 사람의 수에 비유할 수 있습니다.

체계적인 속도 개선을 위해서는 벤치마크를 설정해야 합니다. Bert 분류기에 대해 선택한 벤치마크에는 다음과 같은 두 가지 구성 요소가 있습니다.

  • 대기 시간: 하나의 추론 요청을 처리하는 데 걸리는 시간의 중간값 (99번째 백분위 수 이상의 내부 벤치마크도 있음)
  • 처리량: 1초 안에 처리할 수 있는 추론 수

처리량의 일관된 비교를 위해 벤치마크 코드는 정확히 32개의 작업자 프로세스를 실행했으며 각 프로세스는 1000개의 추론 요청을 순차적으로 처리했습니다. 이 벤치마크 테스트는 36개의 제온 스케일러블 프로세서 코어가 있는 단일 서버에서 실행되었습니다. 이 벤치마크 테스트는 36개의 제온 스케일러블 프로세서 코어가 있는 단일 서버에서 실행되었습니다.

또한 작업자 프로세스 풀에 의해 텍스트 분류 서비스가 지원되는 프로덕션 환경을 시뮬레이션하는 작업을 수행했습니다. 테스트 착수 후 각 작업자는 계속해서 연속적인 추론 요청을 처리해야 했습니다. 실제 프로덕션 환경에서는 여러 머신에 수백 명의 작업자가 분산되어 있으므로, 20ms 미만의 중간 대기 시간으로 초당 25,000건이 넘는 요청을 처리할 수 있습니다.

툴링

미세 조정된 Bert 모델은 Huggingface Transformers 라이브러리 v2.3.0을 사용하여 개발되었습니다. 특히 PyTorch 지원을 위해 특별히 Huggingface를 선택했으며, 현재 프로덕션 또한 PyTorch 1.4.0에서 진행 중입니다. Huggingface 사용으로 인해 Roberta, XLNet, DistilBert, Albert과 같은 다양한 Transformer 기반 모델을 손쉽게 실험할 수 있었습니다. 그리고 이를 통해 추론 속도를 극적으로 높일 수 있는 DistilBert를 발견하기에 이르렀습니다(이제 곧 이에 대해 논의하려고 합니다).

첫째, 추론에 CPU와 GPU 중 어느 것을 선택할 것인가?

첫 번째로 해결해야 할 일은 Bert 기반 텍스트 분류기의 추론 작업을 CPU 혹은 GPU 중 어느 것에서 실행할지에 대한 여부를 결정하는 것이었습니다.

우리의 모델 학습 과정에서 GPU의 처리 속도는 의심할 여지없이 CPU보다 훨씬 빠릅니다. 텍스트 분류 Bert 모델을 미세 조정(학습)하는 데 드는 소요 시간은 GPU보다 CPU에서 10배 이상 더 오래 걸렸으며, Tesla V100 GPU와 비슷한 가격의 36코어 제온 스케일러블 CPU 기반 서버를 사용했을 때에도 마찬가지였습니다.

추론의 관점에서 GPU와 CPU 사이에서의 선택은 응용 프로그램에 따라 다릅니다. 하지만 우리는 다음과 같은 요소로 인해 GPU가 아닌 CPU를 사용하기로 결정했습니다.

  • 단순성. GPU는 입력이 일괄 처리될 때 최적의 스케일을 구현합니다. 그러나 텍스트 분류기는 실시간의 요청-응답 환경에서 작동합니다. 이러한 실시간 요청을 일괄 처리할 경우 오버헤드와 복잡성만 증가하게 됩니다. CPU에서 Bert 모델은 입력을 일괄 처리하지 않아도 매우 잘 작동하므로, CPU에서 추론을 실행하면 더욱 단순한 경로를 통해 처리할 수 있게 됩니다.
  • 경제적 혜택. 텍스트 분류기의 경우 비용 경제학적인 측면에서 CPU에서의 추론이 GPU에서보다 더 유리합니다. 이 글에서 언급한 확장 최적화 작업을 모두 적용한 후 CPU에서의 텍스트 분류 서비스의 처리량은 GPU에 비해 달러당 6배 더 높은 것으로 확인되었습니다. 보다 구체적으로 말하자면, Bert 기반 서비스를 확장한 후 인텔 제온 스케일러블 36코어 서버에서는 초당 3,000개 이상의 추론을 수행할 수 있었던 반면, 비슷한 비용이 드는 Tesla V100 GPU에서는 초당 500개의 추론만 수행할 수 있었습니다.

권장 CPU

벤치마크를 통해 2세대 인텔 제온 스케일러블 프로세서가 딥 러닝 추론에 대한 탁월한 지원으로 인해 작업에 가장 잘 적합한 것으로 나타났습니다. 예를 들어, 2세대 제온 스케일러블 프로세서에 탑재된 VNNI(Vector Neural Network Instructions) 및 이중 FMA(Fused-Multiply-Add) 코어 덕분에 작업 속도가 크게 향상된 것을 확인할 수 있었습니다. 이 글에서 언급하는 모든 결과는 인텔 제온 스케일러블 프로세서 시리즈 칩을 사용해 얻은 것입니다. 다양한 칩에서 테스트한 것이 아니기 때문에 다른 CPU 구성의 경우 상이한 결과가 나올 수 있습니다.

스레드 조정

CPU에서 Bert 추론을 스케일하며 마주한 첫 번째 난관은 여러 작업자 프로세스가 동시에 모델 추론을 수행하기 전에 먼저 PyTorch에 대한 스레드를 적절히 조정해야 한다는 것이었습니다. 이는 각 프로세스 과정에서 PyTorch 모델은 단일 추론 요청을 처리할 때조차 여러 개의 코어를 사용하려고 하기 때문입니다. 따라서 각 프로세스는 동일하게 제한된 리소스(물리적 코어)를 놓고 경쟁하게 됩니다. 이로 인해 동일한 시스템에서 너무 많은 작업자가 동시에 실행되어 정체가 발생합니다.

다행히 이 문제에 대한 해결책은 간단합니다. 아래 그림의 9번째 줄에 보이는 것처럼, 각 추론을 수행하는 작업자 프로세스에서 스레드 수를 1로 설정하기만 하면 됩니다.

이 작업의 영향은 다음과 같습니다. 즉 동일한 컴퓨터에서 여러 작업자 프로세스를 실행하는 경우 각 작업자가 하나의 코어만 사용하므로 스케일링이 더욱 질서 정연하게 수행됩니다. 이로 인해 하나의 기기에서 여러 프로세스를 수행할 수 있으므로, 허용 가능한 대기 시간을 유지하면서 각 기기의 처리량을 늘릴 수 있습니다.

시나리오 1: Bert 베이스라인

첫 번째 베이스라인은 기본 Bert 텍스트 분류 모델이며 이는 첫 Bert 논문에 설명된 아키텍처입니다. 이 Bert 모델은 Huggingface Transformers 2.3.0 라이브러리의 BertForSequenceClassication Pytorch 모델을 사용해 구현되었습니다. 이 시나리오에서는 의도적으로 단순한 설계를 선택하여, 모든 텐서 입력을 고정된 길이의 128 토큰으로 제로 패딩했습니다. 이는 모든 입력의 길이가 동일하도록 입력을 일괄 처리할 때 제로 패딩을 수행해야 하기 때문입니다. 배치 크기가 1인 경우에도 의도치 않게 입력을 제로 패딩하기 쉽지만, 이 베이스라인 시나리오에서 수행할 때의 성능에 대한 영향을 수치화하고자 하였습니다.

아래의 분홍색 상자는 이 ‘Bert 베이스라인’ 시나리오에 대한 벤치마크 결과를 보여 줍니다. 330ms에서조차도 대기 시간이 프로덕션 요구에 충분하지 않으며, 벤치마크에 사용되는 코어 수를 고려하면 처리량이 매우 저조합니다.

시나리오 2: 작은 모델 (DistilBert)

Bert가 2018년 10월 출시된 후, 매개 변수가 더 많은 다양한 모델의 개발을 통해 Bert의 수준을 높이려는 시도가 계속되어 왔습니다. 원래 Bert에는 1억 1천만 개의 매개 변수가 있으며, 이러한 최신 모델 중 일부에는 10억 개가 넘는 매개 변수가 있는 경우도 존재합니다. 이 모델들의 통계적 성능은 Bert보다 현저히 높고 계산 성능은 매우 떨어집니다. 그러나 우리가 원하는 것은 약간의 통계적 성능 저하를 감수하더라도 더 나은 계산 성능을 얻는 것이었습니다.

다행히도 Sanh 등은 2019년 10월, Bert와 같은 대형 모델의 추세를 역전시키는 다양한 모델 중 하나인 DistilBert를 내놓았습니다. 이러한 ‘작은 모델’은 Bert의 크기를 줄이는 데 중점을 두었으며, 이로 인해 통계적 성능의 손실은 최소화하면서도 추론과 학습 속도는 대폭 향상되었습니다.

DistilBert를 선택한 두 가지 이유는 다음과 같습니다. 첫째, DistilBert는 Bert보다 약 두 배 빠르며, 그럼에도 텍스트 분류에 대한 통계적 성능(F1 점수)은 Bert의 1% 이내였습니다. 둘째, Huggingface Transformers 라이브러리 덕분에 DistilBert와 Bert의 전환이 매우 용이했고, 기존의 학습 및 추론 파이프라인에는 전혀 영향을 주지 않았습니다.

다음은 작은 DistilBert 모델을 사용하여 얻은 벤치마크 결과입니다. 처리량을 거의 두 배로 늘리면서도 대기 시간은 거의 절반으로 감소한 것을 확인할 수 있습니다.

시나리오 3: 작은 입력 (동적 셰이프)

다음 개선 사항은 다른 무언가를 더 작게 만드는 작업을 통해 이루어졌습니다. 바로 DistilBert 모델에 대한 텐서 입력입니다.

먼저 제로 패딩을 도입합니다. 딥 러닝 모델은 일반적으로 텐서 배치를 입력으로 사용합니다. 텍스트 분류기의 경우 Bert 토큰화를 통해 텍스트 입력을 텐서로 변환한 다음 텐서를 배치 처리해야 합니다. 텍스트의 길이는 불확실하므로, 다음과 같이 가변 길이 텐서를 제로 패딩하여 고정된 셰이프 배치를 생성해야 합니다.

앞서 언급했듯이 텍스트 분류기는 실시간 요청-응답 환경에서 실행됩니다. 따라서 이상적인 배치 크기는 1입니다. 배치 크기가 1일 때는 모든 텐서의 크기가 동일하므로 텐서를 제로 패딩할 필요가 없다는 것을 알 수 있습니다. 입력 텍스트를 제로 패딩 없이 별도의 배치로 모델에 제공하면 다음과 같은 결과를 얻게 됩니다.

단일 배치에서 가변 길이 텐서를 허용하기 때문에 이러한 입력을 ‘동적 셰이프’라고 부릅니다. 동적 셰이프 입력을 통해 처리량과 대기 시간을 크게 향상시킬 수 있었습니다. 제로 패딩을 수행하지 않고 모델의 입력을 더 작게 만들었기 때문에 이는 직관적으로 납득할 수 있습니다. 또한 동적 셰이프를 사용하든 제로 패딩을 사용하든 모델 출력 확률은 같기 때문에 F1 점수에 부정적인 영향을 미치지 않습니다.

다음은 DistilBert 및 동적 셰이프를 함께 사용한 후의 벤치마크 결과입니다. 처리량은 두 배로 증가하면서도 대기 시간은 절반 이상 감소한 것을 확인할 수 있습니다.

시나리오 4: 작은 가중치 (양자화)

가장 괄목할 만한 성능 향상은 양자화라는 기술을 통한 가중치의 정밀도 감소로 달성할 수 있었습니다.

양자화란 딥 러닝 계산의 효율성을 높이기 위해 모델을 경량화해 사용하는 것을 의미합니다. 예를 들면 32비트 부동 소수점 가중치를 8비트 정수로 표시하는 것을 들 수 있습니다. DistilBert 모델에 활용하는 특정 양자화 기법을 ‘동적 양자화‘라고 합니다. 이 기술은 학습 중 양자화(양자화 인식 학습이라고 함)와는 대조적으로 학습 후 가중치를 양자화합니다.

Pytorch 1.3+에서 동적 양자화 지원의 구현은 매우 용이했으며, 코드에서 한 줄만 변경하면 DistilBert 모델의 모든 선형 계층에서 8비트 가중치 정수 표현을 사용할 수 있었습니다.

아래 그림은 양자화가 원래 DistilBert 모델을 어떻게 변경하는지 보여줍니다. PyTorch는 어텐션 메커니즘의 쿼리(Q), 키(K), 값(V) 계층과 같은 ‘선형’ 계층을 내부 곱셈 및 덧셈 연산을 위해 양자화된 8비트 정수를 사용하는 ‘동적 양자화 선형’ 계층으로 대체합니다.

다음은 Distilbert 및 동적 셰이프, 동적 양자화를 사용할 때의 벤치마크 결과입니다. 이러한 세 가지 최적화의 결과로 인해 대기 시간과 처리량 모두 기본 Bert 베이스라인에 비해 30배 향상되었음을 알 수 있습니다.

또한 양자화 후 F1 점수는 < 1%으로 약간 감소한 것으로 나타났지만, 처리량 및 대기 시간이 향상되었기에 이는 충분한 가치가 있습니다.

시나리오 5: 적은 수의 요청 (캐시)

최종적으로 이룬 최적화 중 하나는 DistilBert 모델로 전송되는 요청 수를 효과적으로 줄이는 것이었습니다. 이는 토큰 ID를 키로 사용해 일반적인 텍스트 입력에 대한 응답을 캐싱함으로써 달성할 수 있었습니다. 동일한 텍스트 입력에 대한 모델의 응답은 항상 동일하기 때문에 이는 가능합니다. 또한 하부 텍스트의 분포가 불규칙할수록 이 방법은 더 효과적입니다.

프로세스 메모리에 100만 개의 데이터를 캐싱할 때 프로덕션 환경에서 캐시 적중률은 40%에 달했습니다. 캐시 적중률은 실제 추론 비용이 0임을 의미하므로, 캐시는 전체 처리량을 거의 두 배로 늘린 것이라 할 수 있습니다.

캐싱의 영향력은 데이터에 따라 다르므로 이는 벤치마크에 포함하지 않았습니다. 그러나 단순한 캐시임에도 심각한 영향을 미칠 수 있음은 유념해야 합니다.

수평적 확장성에 대한 참고 사항

이 글에서 언급한 모든 벤치마크 결과는 동일한 서버에서 32명의 작업자를 동시에 실행해 얻은 것입니다. 여러 서버를 사용해 작업자 수를 수평으로 확장하면 프로덕션 환경에서 하루에 10억 건 이상의 요청을 처리할 수 있습니다.

앞으로의 발전 가능성

지금까지 Bert PyTorch 모델의 개발에서 출시 과정에 이르기까지 최적화에 필요한 여러 핵심 사항에 대해 논의했습니다. 앞으로의 확장성 전략에 영향을 미치는 이 분야에서는 실제로 많은 혁신이 일어나고 있습니다. 예를 들어, 우리는 Onnx Runtime이 비양자화 벤치마크에 근접하는 탁월한 성능을 발휘하므로 면밀히 관찰하고 있습니다. 이 글을 작성할 당시, Microsoft는 이미 Onnx Runtime Bert 최적화의 소스 코드를 공개하고 있었습니다. 또한 인텔과 긴밀히 협력하며 OpenVino의 잠재적인 개선 사항을 모색하고 있습니다.

요점 및 결론

우리가 내린 결론은 특별히 실시간 텍스트 분류를 위해 CPU에서 Distilbert 또는 Bert를 대규모로 확장할 수 있다는 것입니다. 이를 위해 작은 모델(DistilBert), 작은 입력(동적 셰이프) 및 가중치의 경량화(양자화)를 통해 Bert 텍스트 분류 대기 시간 및 처리량을 30배 이상 개선하는 방법에 대해 기술했습니다. 이제 딥 러닝 분류기를 도입하여 CPU에서 합리적인 비용 및 20ms 미만의 중간 대기 시간으로 하루에 10억 건 이상의 요청을 처리할 수 있게 되었습니다. 이는 더할 수 없이 만족스러운 결과였습니다.

이 모든 기술에 대한 유용한 조언을 제공해 주신 Edin Mulalić님 및 Saša Anđelković님께 감사드립니다.


Roblox Corporation이나 이 블로그는 특정 회사나 서비스를 홍보하지 않습니다. 또한 블로그에 포함된 정보의 정확성, 신뢰성, 완전성에 대하여 어떠한 보장이나 약속도 하지 않습니다.

이 블로그 게시물은 Roblox 테크 블로그에 수록된 글입니다.