추론 최적화는 프로덕션에 배포된 생성형 AI 애플리케이션에서 매우 중요한 부분입니다. 대규모로 LLM을 효율적으로 사용하는 것은 어려운 일이며, 지난 몇 년 동안 추론을 더 빠르고 저렴하게 하기 위해 많은 기술이 개발되었습니다. 이 글에서 이러한 기법들을 살펴보겠습니다.
대규모 언어 모델(LLM)은 모두 Vaswani 등이 2017년에 발명한 트랜스포머 아키텍처를 기반으로 합니다. 트랜스포머 아키텍처는 다양한 언어 작업에서 뛰어난 정확도, 단발성 학습, 인간에 가까운 능력을 달성합니다. 그러나 이러한 기반 모델은 종종 수백억에서 수천억 개의 매개변수로 구성되어 있어 추론 과정에서 훈련 비용이 많이 들고 리소스 집약적입니다. 추론 비용은 입력 컨텍스트가 길어질수록 증가하며, 대량의 입력 데이터로 인해 상당한 처리 능력이 요구됩니다. 따라서 효율적인 추론은 특히 메모리와 컴퓨팅 리소스를 관리하는 데 있어 매우 중요한 과제입니다.

보다 구체적으로, 가장 잘 알려진 LLM은 GPT-3, GPT-4, LLaMA, Mistral, DeepSeek 등과 같은 디코더 전용 LLM입니다. 이러한 모델은 인과 관계 모델링 작업에 대해 사전 학습되어 다음 단어 예측자 역할을 합니다. 이러한 모델은 일련의 토큰을 입력으로 처리하고 정지 조건에 도달할 때까지 자동 회귀적으로 다음 토큰을 생성합니다.
디코더 전용 모델의 LLM 추론에는 프리필 단계와 디코드 단계라는 두 가지 주요 단계가 포함됩니다. 프리필 단계에서 모델은 입력 토큰을 처리하여 첫 번째 새 토큰을 생성하기 위한 중간 상태(키와 값)를 계산합니다. 이 단계는 행렬 매트릭스 연산과 유사하며 고도로 병렬화되어 GPU 기능을 효율적으로 활용합니다. 반대로 디코딩 단계에서는 이전 토큰의 상태에 따라 한 번에 하나씩 토큰을 생성합니다. 이 행렬-벡터 연산은 연산 속도보다는 GPU로의 데이터 전송이 주로 지연 시간을 결정하기 때문에 메모리에 종속되어 GPU 컴퓨팅 성능을 제대로 활용하지 못합니다.
디코딩 단계를 최적화하는 것은 추론 문제를 해결하기 위한 핵심 요소입니다. 효율적인 주의 메커니즘을 개발하고 키와 값을 더 잘 관리하여 메모리 병목 현상을 줄이는 것이 해결책이 될 수 있습니다. 이 포스팅에서는 트랜스포머 아키텍처와 주의 메커니즘에 대한 기본적인 이해가 있다는 가정 하에 추론 성능을 향상시키기 위한 실용적인 접근법을 중점적으로 다룹니다. 이러한 최적화는 실제 LLM 배포에서 처리량을 개선하고 지연 시간을 줄이는 데 매우 중요합니다.
또 다른 문제는 토큰 비교 가능성에 영향을 미치는 LLM 간에 서로 다른 토큰화 도구를 사용하는 데서 발생합니다. 대략 영어 문자 4개에 해당하는 토큰은 토큰라이저에 따라 표현 방식이 달라지기 때문에 추론 처리량(예: 초당 토큰 수)을 직접 비교하는 것은 오해의 소지가 있습니다. 이러한 가변성은 추론 중 LLM 성능을 정확하게 평가하고 비교할 수 있는 표준화된 평가 지표의 필요성을 강조합니다.
일괄 처리는 대규모 언어 모델(LLM)에서 GPU 사용률과 처리량을 개선하기 위한 핵심 전략입니다. 일괄 처리는 동일한 모델을 사용하여 여러 요청을 동시에 처리함으로써 모델 가중치의 메모리 비용을 요청 간에 분산시켜 더 큰 배치가 더 많은 GPU 컴퓨팅 성능을 활용할 수 있도록 합니다. 그러나 배치 크기에는 한계가 있으며, 지나치게 큰 배치는 특히 키-값(KV) 캐싱과 관련된 LLM의 메모리 요구로 인해 메모리 오버플로를 유발할 수 있습니다(자세한 내용은 나중에 설명).

기존 또는 정적 일괄 처리는 일괄 처리 내의 요청이 서로 다른 수의 완료 토큰을 생성하여 다양한 실행 시간을 초래하는 경우가 많기 때문에 한계가 있습니다. 이로 인해 모든 요청이 가장 느린 요청이 완료될 때까지 대기하게 되며, 이는 생성 길이가 크게 다를 때 문제가 될 수 있습니다. 이 문제를 해결하기 위해 성능을 최적화하기 위해 인플라이트 배치와 같은 고급 기술이 개발되었습니다.
연속 배치라고도 하는 기내 배치는 간단한 챗봇 응답부터 복잡한 문서 요약 또는 코드 생성에 이르기까지 다양한 LLM 워크로드의 동적 특성으로 인해 발생하는 문제를 해결합니다. 이러한 작업은 크기가 매우 다른 결과물을 생성하므로 요청을 효율적으로 일괄 처리하고 병렬로 실행하기가 어렵습니다. 정적 일괄 처리와 달리 기내 일괄 처리를 사용하면 서버가 완료된 시퀀스를 일괄 처리에서 즉시 제거하고 다른 요청이 진행 중인 동안 새 요청 처리를 시작할 수 있습니다. 이 접근 방식은 실제 시나리오에서 요청의 다양한 실행 시간에 적응하여 GPU 활용도를 크게 향상시킵니다.
모델 병렬화는 대규모 머신 러닝 모델을 여러 GPU에 분산하여 메모리와 연산 수요를 관리하는 데 중요한 전략입니다. 이 접근 방식을 사용하면 단일 장치의 메모리 용량을 초과하는 대규모 모델이나 입력 배치를 처리할 수 있으므로 메모리 제약이 엄격한 경우 학습과 추론 모두에 필수적입니다. 파이프라인 병렬 처리, 텐서 병렬 처리, 시퀀스 병렬 처리 등 모델 가중치를 분할하는 다양한 기법이 존재하며, 각 기법은 모델 분포의 다양한 측면을 다룹니다. 훈련 중에 더 큰 입력 배치를 처리하기 위해 여러 기기에 걸쳐 모델 가중치를 복제하는 데 중점을 두는 데이터 병렬 처리와 달리, 이러한 방법은 훈련과 추론 모두에서 메모리 사용량을 줄이는 데 더 적합합니다.

파이프라인 병렬화는 모델을 수직으로 순차적인 청크로 나누고, 각 청크에는 별도의 디바이스에 할당된 레이어의 하위 집합이 포함됩니다. 예를 들어, 4방향 파이프라인 설정에서 각 디바이스는 모델 레이어의 1/4을 처리하여 출력을 다음 디바이스로 순차적으로 전달합니다. 이렇게 하면 디바이스당 메모리 요구량이 크게 줄어들지만, 이전 레이어의 출력을 기다리는 동안 디바이스가 유휴 상태가 되는 '파이프라인 버블'이라는 비효율적인 문제가 발생합니다. 순차 처리를 위해 입력 배치를 더 작은 하위 배치로 분할하는 마이크로배칭은 이러한 버블을 줄일 수 있지만, 정방향 및 역방향 통과 중에 유휴 시간이 지속되므로 완전히 제거할 수는 없습니다.
반면 텐서 병렬 처리는 개별 레이어를 수평으로 분할하여 여러 디바이스에서 독립적으로 실행할 수 있는 작은 계산 블록으로 분할합니다. 이는 예를 들어 병렬 연산을 위해 서로 다른 주의 헤드를 별도의 장치에 할당할 수 있는 주의 블록 및 다층 퍼셉트론(MLP)과 같은 트랜스포머 구성 요소에 특히 효과적입니다. 그러나 텐서 병렬화는 레이어노름이나 드롭아웃과 같이 쉽게 분할할 수 없고 여러 디바이스에서 복제해야 하는 연산에서는 효율성이 떨어지며, 이로 인해 활성화 저장을 위한 메모리 사용량이 중복될 수 있습니다. 이러한 한계는 메모리 효율성을 최적화하기 위한 보완적인 접근 방식의 필요성을 강조합니다.
시퀀스 병렬화는 입력 시퀀스 차원을 따라 분할하여 시퀀스 요소 간의 독립성을 활용함으로써 레이어노름과 드롭아웃과 같은 연산에서 발생하는 메모리 비효율성을 해결합니다. 이 방법은 중복 활성화로 인한 메모리 사용 공간을 줄여주므로 텐서 병렬화를 보완하는 데 유용합니다. 이러한 병렬화 기법은 상호 배타적이지 않으며 결합하여 대규모 언어 모델(LLM)을 더욱 최적화할 수 있습니다. 또한 주의 모듈에 대한 특정 최적화 전략은 확장성을 향상시키고 GPU당 메모리 요구량을 줄여 대규모 모델에 대한 보다 효율적인 훈련과 추론을 가능하게 합니다.
2017년 논문 *주의만 있으면 충분합니다*에서는 자기 주의를 초석으로 하는 Transformer 모델을 소개했습니다. 자기 주의를 통해 이 모델은 문장에서 서로 다른 단어의 관련성을 평가하여 자연어 처리와 같은 작업의 문맥 이해를 향상시킬 수 있습니다. 이 논문은 특히 쿼리와 키-값 쌍을 출력에 매핑하는 확장된 도트-제품 주의(SDPA) 메커니즘을 통해 자기 주의를 공식화하여 최신 신경망의 중추적인 구성 요소로 만들었습니다. 주의 계산을 최적화하는 가장 중요한 몇 가지 기술을 소개합니다:

다중 헤드 주의(MHA)는 쿼리, 키, 값 행렬의 각각 다른 투영을 가진 여러 주의 연산을 병렬로 실행하여 SDPA를 기반으로 합니다. 이러한 병렬 연산, 즉 '헤드'는 서로 다른 표현 하위 공간에 집중하여 입력에 대한 모델의 이해를 강화합니다. 이러한 헤드의 출력은 연결되고 선형적으로 투영되므로 각 헤드의 차원을 줄임으로써(예: 모델 차원을 헤드 수로 나누어 8과 같이) 단일 헤드 주의와 비슷한 계산 효율성을 유지할 수 있습니다.
다중 쿼리 관심(MQA)은 여러 쿼리 예측을 유지하면서 여러 관심 헤드에서 키 및 값 예측을 공유하여 추론을 위한 MHA를 최적화합니다. 이렇게 하면 메모리 대역폭 요구량과 키-값(KV) 캐시 크기가 줄어들어 배치 크기가 커지고 컴퓨팅 활용도가 향상됩니다. 그러나 MQA는 정확도를 약간 떨어뜨릴 수 있으며, 이를 활용하는 모델은 성능을 유지하기 위해 MQA를 활성화한 상태에서 학습 또는 미세 조정이 필요합니다.
그룹화된 쿼리 주의(GQA)는 쿼리 헤드를 그룹화하고 각 그룹 내에서 키 값 예측을 공유함으로써 MHA와 MQA의 균형을 맞추고, MQA에 가까운 계산 효율로 MHA에 가까운 품질을 달성합니다. Llama 2 70B와 같은 모델은 GQA를 사용하며, MHA로 훈련된 모델은 최소한의 추가 훈련만으로 GQA에 적응할 수 있습니다. 캐시 관리의 추가 최적화가 필요하지만 MQA와 GQA 모두 KV 캐시 메모리 수요를 줄여줍니다.
플래시어텐션은 GPU 메모리 계층 구조를 보다 효과적으로 활용하기 위해 계산 순서를 재지정함으로써 주의 메커니즘을 향상시킵니다. 기존의 레이어별 처리와 달리 플래시어텐션은 연산을 융합하고 '타일링'을 사용해 출력 행렬의 작은 부분을 한 번에 계산함으로써 메모리 읽기/쓰기 연산을 최소화합니다. 이 I/O를 인식하는 정확한 어텐션 알고리즘은 수정 없이 기존 모델에 원활하게 통합되어 데이터 이동을 최적화함으로써 상당한 속도 향상을 제공합니다.
KV 캐싱은 대규모 언어 모델(LLM)의 디코딩 단계에서 자기 주의 계산의 효율성을 개선하기 위해 사용되는 중요한 최적화 기법입니다. 이 단계에서 생성되는 각 토큰은 프리필 단계와 후속 디코딩 단계에서 계산된 토큰을 포함하여 모든 이전 토큰의 키(K) 및 값(V) 텐서에 따라 달라집니다. 각 시간 단계에서 모든 토큰에 대해 이러한 텐서를 다시 계산하는 대신, KV 캐싱은 GPU 메모리에 저장하고 계산되는 대로 새 텐서를 캐시에 추가합니다. 일반적으로 모델의 각 계층에 대해 별도의 KV 캐시가 유지되므로 중복 계산이 크게 줄어들고 디코딩 프로세스의 속도가 빨라집니다.

GPU에서 LLM의 메모리 요구 사항은 주로 모델 가중치와 KV 캐시라는 두 가지 구성 요소에 의해 결정됩니다. 모델 가중치는 모델의 파라미터로 구성되며 상당한 메모리를 차지합니다. 예를 들어 16비트 정밀도의 Llama 2 7B와 같은 70억 개의 파라미터를 가진 모델은 약 14GB가 필요합니다. 반면에 KV 캐시는 재계산을 피하기 위해 자체 주의 텐서를 저장하며, 그 크기는 레이어 수, 주의 헤드, 헤드 크기 및 정밀도와 같은 요소에 따라 결정됩니다. 각 토큰에 대해 캐시 크기는 2 * num_layers * (num_heads * dim_head) * precision_in_bytes로 계산되며, 여기서 2의 계수는 K 행렬과 V 행렬을 모두 고려합니다. 입력 배치의 경우, 총 KV 캐시 크기는 배치 크기와 시퀀스 길이에 따라 확장되며, 시퀀스 길이가 4,096이고 배치 크기가 1인 Llama 2 7B 모델의 경우 ~2GB와 같이 상당한 크기에 도달할 수 있습니다.
배치 크기와 시퀀스 길이에 따라 선형적으로 증가하여 처리량을 제한하고 긴 컨텍스트 입력을 복잡하게 처리할 수 있기 때문에 KV 캐시를 효율적으로 관리하는 것은 어려운 과제입니다. 일반적인 비효율성은 실제 입력 크기와 관계없이 지원되는 최대 시퀀스 길이(예: 2,048토큰)를 위해 메모리가 예약되는 정적 오버프로비저닝에서 발생합니다. 이는 상당한 메모리 낭비 또는 조각화로 이어지며, 예약된 공간의 상당 부분이 요청의 수명 내내 사용되지 않은 채로 남아 귀중한 GPU 메모리 리소스를 묶어두는 경우가 많기 때문입니다.
이러한 비효율성을 해결하기 위해 PagedAttention 알고리즘은 운영 체제 페이징에서 영감을 얻은 새로운 접근 방식을 도입했습니다. 이 알고리즘은 KV 캐시를 각각 정해진 수의 토큰을 나타내는 고정 크기 블록으로 나누어 메모리에 비연속적으로 저장할 수 있습니다. 블록 테이블은 이러한 블록을 추적하여 주의 계산 중에 필요에 따라 블록을 가져옵니다. 새 토큰이 생성되면 추가 블록이 동적으로 할당됩니다. 이 방법은 연속 할당 및 오버프로비저닝의 필요성을 제거하여 메모리 낭비를 최소화하고, 더 큰 배치 크기를 가능하게 하며, 처리량을 개선하여 LLM의 KV 캐시 메모리 관리에서 상당한 발전을 이룹니다.
이 섹션에서는 메모리 소비를 줄이고 GPU의 성능을 향상시키기 위해 대규모 언어 모델(LLM)을 최적화하는 다양한 기법에 대해 설명합니다. 주요 방법으로는 정량화, 희소성, 증류가 있으며, 각각 모델 효율성의 다양한 측면을 목표로 합니다. 이러한 기술은 모델 가중치를 수정하고, GPU 하드웨어 가속을 활용하고, 지식을 더 작은 모델로 전송하여 성능을 유지하면서 더 큰 모델을 제한된 하드웨어에서 실행할 수 있게 해줍니다. 이러한 방법은 모델의 정확도를 저하시킬 수 있으므로 신중하게 사용해야 합니다.
정량화는 모델의 가중치와 활성화의 정밀도를 일반적으로 32비트 또는 16비트에서 8비트 이하로 줄여주므로 모델이 메모리를 덜 차지하고 데이터를 더 효율적으로 전송할 수 있습니다. 가중치를 정량화하는 것은 학습 후 고정된 특성으로 인해 간단하지만, 활성화를 정량화하는 것은 동적 범위를 확장하는 이상값으로 인해 더 복잡합니다. LLM.int8()과 같은 기술은 특정 활성화에 더 높은 정밀도를 선택적으로 적용하거나 활성화에 양자화된 가중치의 동적 범위를 재사용하여 이 문제를 해결하지만, GPU에서는 연산을 위해 가중치를 다시 더 높은 정밀도로 변환해야 할 수 있습니다.
희소성은 모델 값을 0에 가깝게 잘라내어 메모리를 덜 필요로 하는 희소 행렬을 생성하는 것을 포함합니다. GPU는 4개의 값 중 2개를 0으로 표현하는 등 구조화된 희소성을 지원하여 계산을 가속화합니다. 희소성과 양자화를 결합하면 실행 속도를 더욱 향상시킬 수 있습니다. LLM을 위한 최적의 희소성 표현에 대한 연구가 계속되고 있으며, 이는 추론 속도를 개선할 수 있는 유망한 방법임을 시사합니다.
Distilation은 더 큰 '교사' 모델에서 더 작은 '학생' 모델로 지식을 이전하여 성능을 유지하면서 크기를 압축합니다. 예를 들어 DistilBERT는 BERT에 비해 크기는 40% 줄고 속도는 60% 향상되었지만 기능은 97% 그대로 유지합니다. 증류에는 교사의 결과물을 모방하거나 교사가 생성한 데이터를 교육에 사용하는 "단계별 증류!"와 같은 방법이 포함될 수 있으며, 효율적인 학습을 위한 근거를 통합합니다. 그러나 많은 고급 LLM의 제한적인 라이선스로 인해 증류에 적합한 교사 모델을 사용할 수 없습니다.
추측적 샘플링 또는 보조 생성이라고도 하는 추측적 추론은 일반적으로 토큰별로 텍스트를 생성하는 GPT 스타일 모델과 같은 자동 회귀 대규모 언어 모델(LLM)의 실행을 병렬화하는 방법입니다. 표준 실행에서는 각 토큰이 컨텍스트에 대한 모든 이전 토큰에 의존하므로 (n+1)번째 토큰이 생성되기 전에 n번째 토큰을 생성해야 하므로 병렬 생성이 불가능합니다. 추측 추론은 "더 저렴한" 초안 모델을 사용하여 여러 개의 미래 토큰을 동시에 예측한 다음 메인 모델에서 병렬로 확인하거나 거부함으로써 이 문제를 해결하여 더 빠른 텍스트 생성을 가능하게 합니다.
이 프로세스에는 리소스 집약적이지 않은 방법을 사용하여 여러 토큰의 연속 초안을 생성한 다음, 초안을 투기적 컨텍스트로 사용하는 메인 모델에서 병렬 검증을 수행하는 과정이 포함됩니다. 검증 모델이 초안 토큰과 일치하면 승인되고, 그렇지 않으면 일치하지 않는 토큰과 후속 토큰은 폐기되고 새로운 초안으로 프로세스가 반복됩니다. 초안 토큰은 여러 모델을 훈련하거나, 사전 훈련된 모델에서 여러 헤드를 미세 조정하여 미래 토큰을 예측하거나, 더 크고 더 많은 기능을 갖춘 검증 모델과 함께 작은 초안 모델을 사용하는 등 다양한 접근 방식을 사용하여 생성할 수 있으며, 각각 고유한 장단점을 가지고 있습니다.
분리 추론은 성능, 비용 및 리소스 사용을 최적화하기 위해 계산 작업을 여러 하드웨어로 분할하는 기술입니다. 구체적으로는 프리필링 단계와 디코딩 단계를 분리합니다. 이러한 단계를 분리함으로써 각 단계를 계산 요구 사항에 가장 적합한 하드웨어에 할당하여 효율성과 확장성을 개선할 수 있습니다.

미리 채우기는 컴퓨팅 집약적인 작업으로, 전체 입력 프롬프트를 처리하고 KV 캐시를 생성하기 위해 상당한 행렬 곱셈이 필요합니다. 이 단계에서는 병렬 연산에 탁월한 GPU 또는 TPU와 같은 고성능 하드웨어의 이점을 누릴 수 있습니다. 사전 채우기는 추론 요청당 일회성 작업이므로 이러한 워크로드에 최적화된 중앙 집중식 강력한 컴퓨팅 노드로 오프로드할 수 있습니다. 이 설정은 대용량 프롬프트를 더 빠르게 처리하고 성능이 낮은 디바이스의 부담을 줄여주므로 처리량이 많은 하드웨어를 사용할 수 있는 클라우드 기반 또는 데이터 센터 환경에 이상적입니다.
반면, 디코딩은 메모리 제한이 있고 반복적인 토큰 생성을 수반하며, KV 캐시 액세스에 크게 의존합니다. 연산 능력은 덜 필요하지만 빠른 메모리 액세스가 필요하므로 CPU나 엣지 디바이스처럼 덜 강력하고 메모리에 최적화된 하드웨어에 적합합니다. 디코딩을 온프레미스 서버나 엣지 디바이스와 같이 최종 사용자와 더 가까운 별도의 하드웨어로 옮김으로써 분리 추론은 지연 시간과 네트워크 대역폭 수요를 줄여줍니다. 이렇게 분리하면 프리필은 하이엔드 클라우드 서버에서 실행하고 디코딩은 로컬 또는 엣지 디바이스에서 수행하는 유연한 배포가 가능하므로 리소스 할당을 최적화하고 실시간 챗봇이나 대화형 AI 시스템과 같은 애플리케이션을 효율적으로 확장할 수 있습니다.
최근 LLM의 성능을 개선하기 위해 많은 추론 최적화 기법이 개발되었습니다.
이러한 기술을 구현하려면 LLM 아키텍처와 사용 중인 하드웨어에 대한 깊은 이해가 필요하므로 일반적으로 vLLM, TensorRT-LLM, LMDeploy 등과 같이 이미 이러한 기술을 구현한 기존 추론 엔진을 사용하는 것이 더 쉽습니다. 실제로 저희는 자체 추론 엔진에 이러한 기술을 구현했으며, 자체 모델을 배포하려는 경우 추론 엔진에 대한 블로그 게시물을 작성했습니다: 여기에서 읽을 수 있습니다..
자체 LLM을 직접 배포할 수 없거나 배포하고 싶지 않은 경우, NLP Cloud를 사용하여 프로덕션 환경에서 대규모로 빠르게 생성되는 AI 모델을 활용할 수 있습니다. 지금 NLP Cloud에서 빠른 추론을 사용해 보세요!
추론 엔진 전반에 대해 궁금한 점이 있으시면 언제든지 문의해 주세요!
Julien
NLP 클라우드의 CTO