Natural Language Processing Theory : 효율적인 미세 조정과 추론(Efficient Fine-tuning & Inference)
모델의 크기와 비용
대규모 언어 모델(Large Language Model)은 매개변수가 수십억에서 수천억 개에 이릅니다. 매개변수 하나는 결국 메모리에 저장된 숫자 하나이므로, 모델이 크다는 것은 그만큼 많은 메모리와 연산이 필요하다는 뜻입니다. 모델의 능력은 규모에서 나왔지만, 같은 규모가 모델을 학습하고 실행하는 데 가장 큰 장벽이 됩니다.
매개변수가 70억 개인 모델을 예로 들어 보겠습니다. 매개변수를 32비트 실수로 저장하면 하나에 4바이트이므로, 가중치만으로 28GB가 필요합니다. 이는 추론에 필요한 최소한의 양이며, 학습할 때는 훨씬 많은 메모리가 필요합니다. 최적화 알고리즘(Optimizer)에서 다룬 Adam은 가중치마다 기울기의 이동 평균과 기울기 제곱의 이동 평균을 따로 저장하고, 역전파를 위해 기울기도 저장해야 하기 때문입니다.
일반적인 혼합 정밀도 학습에서는 매개변수 하나당 16비트 가중치(2바이트), 16비트 기울기(2바이트), 32비트 원본 가중치(4바이트), Adam의 두 상태(각 4바이트)를 합쳐 약 16바이트가 필요합니다. 70억 개 매개변수라면 약 112GB이며, 여기에 각 층의 중간 계산 결과까지 더해야 합니다. 가장 큰 GPU 한 장의 메모리로도 감당하기 어려운 양입니다.
이 강좌에서는 이러한 비용을 줄이는 방법을 두 갈래로 나누어 다룹니다. 하나는 모델 전체가 아니라 일부만 학습해 미세 조정 비용을 줄이는 방법이고, 다른 하나는 숫자를 더 적은 비트로 저장하거나 계산을 재사용해 추론 비용을 줄이는 방법입니다.
매개변수 효율적 미세 조정(PEFT)
사전 학습 언어 모델에서는 사전 학습한 모델을 과제에 맞게 미세 조정하는 방식을 다루었습니다. 이때 모든 가중치를 갱신하는 것을 전체 미세 조정(Full Fine-tuning)이라 합니다. 모델이 작을 때는 문제가 없었지만, 수십억 개의 매개변수를 모두 갱신하려면 위에서 계산한 학습 메모리가 그대로 필요합니다.
저장 비용도 문제입니다. 고객 상담, 법률 문서 요약, 코드 리뷰처럼 과제가 열 개라면, 전체 미세 조정은 과제마다 모델 전체의 복사본을 만들어 냅니다. 70억 개 매개변수 모델을 16비트로 저장하면 14GB이므로, 과제 열 개에 140GB가 필요하고 서비스할 때도 과제마다 다른 모델을 메모리에 올려야 합니다.
그런데 미세 조정으로 바뀌는 것은 생각보다 많지 않습니다. 사전 학습으로 얻은 언어 능력은 대부분 그대로 쓰이고, 과제에 맞게 조금만 방향을 틀어 주면 충분한 경우가 많습니다. 그래서 원래 가중치는 얼려 두고, 아주 적은 수의 새 매개변수만 추가해 학습하는 방법들이 등장했습니다. 이를 통틀어 매개변수 효율적 미세 조정(Parameter-Efficient Fine-Tuning, PEFT)이라 합니다.
어댑터(Adapter): 2019년 홀스비(Neil Houlsby) 연구진이 제안한 방법으로, 트랜스포머의 각 층 사이에 작은 신경망을 끼워 넣고 그것만 학습합니다. 입력을 작은 차원으로 줄였다가 다시 늘리는 병목 구조여서 매개변수가 적습니다. 다만 층이 추가되므로 추론할 때 계산이 조금 늘어납니다.프롬프트 튜닝(Prompt Tuning): 입력 앞에 학습 가능한 가상의 토큰 벡터 몇 개를 붙이고 그것만 학습합니다. 사람이 쓰는 프롬프트 문장 대신, 모델에게 가장 잘 통하는 프롬프트를 벡터 공간에서 직접 찾는 셈입니다. 모델이 클수록 전체 미세 조정에 가까운 성능을 냅니다.LoRA: 가중치 행렬의 변화량을 작은 행렬 두 개의 곱으로 나타내 학습합니다. 현재 가장 널리 쓰이는 방법이므로 아래에서 자세히 다룹니다.
LoRA(Low-Rank Adaptation)
LoRA(Low-Rank Adaptation)는 2021년 후(Edward Hu) 연구진이 제안한 방법으로, 미세 조정에 의한 가중치의 변화는 낮은 계수(Rank)로 충분히 표현할 수 있다는 관찰에서 출발합니다. 계수가 낮다는 것은 행렬이 담고 있는 정보의 방향이 몇 개 되지 않는다는 뜻입니다. 미세 조정이 모델을 크게 바꾸는 것이 아니라 몇 가지 방향으로 조금씩 틀어 주는 일이라면, 그 변화량 행렬도 적은 수의 방향으로 표현할 수 있습니다.
원래의 가중치 행렬 \(W\)의 크기가 \(d \times k\)일 때, LoRA는 \(W\)를 얼려 두고 변화량을 두 개의 작은 행렬 \(B\)와 \(A\)의 곱으로 나타냅니다.
\[W' = W + \Delta W = W + \frac{\alpha}{r} B A, \qquad B \in \mathbb{R}^{d \times r}, \quad A \in \mathbb{R}^{r \times k}\]\(r\)은 계수로, 보통 4에서 64 정도의 작은 값을 사용합니다. 크기가 \(4096 \times 4096\)인 가중치 행렬이라면 원래 매개변수는 약 1,677만 개이지만, \(r = 8\)일 때 \(B\)와 \(A\)의 매개변수는 \(4096 \times 8 + 8 \times 4096 = 65{,}536\)개로 원래의 약 0.39%에 불과합니다. 학습할 매개변수가 적으므로 기울기와 Adam 상태도 그만큼만 저장하면 되어, 학습 메모리가 크게 줄어듭니다.
\(B\)는 0으로, \(A\)는 작은 무작위 값으로 초기화합니다. 그러면 학습을 시작하는 순간 \(BA = 0\)이므로 모델은 원래 모델과 정확히 같은 상태에서 출발하고, 학습이 진행되면서 필요한 변화만 조금씩 더해집니다. \(\alpha / r\)은 변화량의 크기를 조절하는 배율로, 계수 \(r\)을 바꿀 때 학습률을 다시 조정하지 않아도 되게 해 줍니다.
LoRA의 큰 장점은 추론할 때 추가 비용이 없다는 점입니다. 학습이 끝나면 \(BA\)를 계산해 \(W\)에 더해 두면 되므로, 모델의 구조와 계산량은 원래와 똑같습니다. 어댑터처럼 층이 늘어나지 않습니다. 반대로 합치지 않고 원래 모델 하나에 과제별 LoRA 가중치만 바꿔 끼우는 것도 가능합니다. 과제가 열 개라도 원래 모델 하나와 수십 MB 크기의 LoRA 가중치 열 개만 있으면 됩니다.
- Tip : LoRA는 보통 셀프 어텐션의 쿼리, 키, 값, 출력 행렬에 적용하며, 피드포워드 층까지 포함하면 성능이 더 오르는 경우가 많습니다. 계수 \(r\)을 키우면 표현력이 늘지만, 일정 수준을 넘으면 성능 향상이 거의 없으므로 작은 값부터 시도하는 것이 좋습니다.
숫자의 정밀도
추론 비용을 줄이는 가장 직접적인 방법은 숫자 하나를 더 적은 비트로 저장하는 것입니다. 이를 이해하려면 먼저 실수가 어떻게 저장되는지 알아야 합니다.
컴퓨터의 실수는 크기의 범위를 정하는 지수(Exponent) 부분과, 정밀도를 정하는 가수(Mantissa) 부분으로 이루어집니다. 32비트 실수 FP32는 지수 8비트와 가수 23비트를 사용합니다. 16비트 실수에는 두 가지가 있습니다. FP16은 지수 5비트와 가수 10비트를 사용해 정밀도는 비교적 높지만, 표현할 수 있는 범위가 좁아 아주 크거나 작은 값을 다룰 수 없습니다. BF16(bfloat16)은 지수를 FP32와 같은 8비트로 유지하고 가수를 7비트로 줄였습니다. 정밀도는 낮지만 FP32와 같은 범위를 표현할 수 있어, 학습 중 기울기가 아주 작아지거나 커져도 넘치지 않습니다.
신경망은 개별 숫자의 정밀도에 생각보다 둔감합니다. 수많은 가중치와 입력을 곱해 더하는 과정에서 작은 오차들이 서로 상쇄되기 때문입니다. 그래서 오늘날 대규모 언어 모델의 학습과 추론은 대부분 BF16으로 이루어지며, 오차가 누적되기 쉬운 일부 계산만 FP32로 수행합니다. 이를 혼합 정밀도(Mixed Precision)라 합니다.
위 그래프는 매개변수 70억 개 모델의 가중치를 각 형식으로 저장할 때 필요한 메모리입니다. 비트 수가 절반이 될 때마다 메모리도 절반이 되며, 4비트 정수로 저장하면 3.5GB로 일반 소비자용 그래픽카드나 노트북에서도 실행할 수 있는 크기가 됩니다. 메모리가 줄면 GPU가 메모리에서 가중치를 읽어 오는 시간도 줄어드는데, 다음 토큰을 하나씩 생성하는 언어 모델의 추론은 계산보다 가중치를 읽어 오는 속도에 묶이는 경우가 많으므로 속도도 함께 빨라집니다.
양자화(Quantization)
양자화(Quantization)는 연속적인 실수 값을 정해진 몇 개의 정수 값으로 바꾸는 것입니다. 8비트 정수라면 -127에서 127 사이의 255개 값만 사용할 수 있으므로, 원래의 실수를 이 중 가장 가까운 값으로 반올림합니다.
가장 단순한 방법은 값들의 최대 절댓값을 기준으로 비율을 정하는 최대 절댓값 양자화(Absmax Quantization)입니다.
가중치가 \([0.5, -1.2, 0.03, 2.54]\)라면 최대 절댓값은 2.54이므로, 각 값에 \(127 / 2.54 = 50\)을 곱해 반올림하면 \([25, -60, 2, 127]\)이 됩니다. 이 정수를 저장해 두고 계산할 때 다시 \(2.54 / 127 = 0.02\)를 곱해 복원하면 \([0.5, -1.2, 0.04, 2.54]\)입니다. 0.03이 0.04가 된 것을 제외하면 거의 그대로 복원되며, 저장 공간은 4분의 1로 줄었습니다.
문제는 이상치(Outlier)입니다. 같은 가중치에 100.0이라는 값이 하나 섞여 있다면, 최대 절댓값이 100이 되어 복원 단위가 \(100 / 127 \approx 0.787\)로 커집니다. 그러면 나머지 값은 \([0.787, -1.575, 0.0, 2.362]\)로 복원되어, 0.5가 0.787이 되고 0.03은 아예 0이 됩니다. 값 하나 때문에 나머지 모든 값의 정밀도가 무너지는 것입니다.
2022년 데트머스(Tim Dettmers) 연구진은 매개변수가 수십억 개를 넘는 언어 모델에서 일부 차원에 매우 큰 값이 체계적으로 나타난다는 것을 보고했습니다. 이 값들을 무시하고 양자화하면 성능이 급격히 떨어지므로, 이상치가 있는 차원만 16비트로 따로 계산하는 방법을 제안했습니다. 이후로도 가중치를 작은 묶음으로 나누어 묶음마다 따로 비율을 정하거나, 양자화 오차를 다른 가중치가 보정하도록 조정하는 GPTQ 같은 방법이 개발되어, 4비트까지 줄여도 성능 저하를 작게 유지할 수 있게 되었습니다.
양자화는 학습이 끝난 모델에 적용하는 학습 후 양자화(Post-Training Quantization)와, 학습 과정에서 양자화 오차를 미리 반영하는 양자화 인식 학습(Quantization-Aware Training)으로 나뉩니다. 대규모 언어 모델은 다시 학습시키는 비용이 크므로 학습 후 양자화가 주로 쓰입니다.
QLoRA
양자화와 LoRA를 결합하면 학습 비용을 더 줄일 수 있습니다. 2023년 데트머스 연구진이 제안한 QLoRA는 얼려 둔 원래 모델을 4비트로 양자화해 메모리에 올리고, 그 위에 16비트 LoRA 가중치를 붙여 학습합니다.
원래 가중치는 학습하지 않으므로 4비트로 저장해 두었다가 계산할 때만 16비트로 복원하면 되고, 기울기와 Adam 상태는 아주 작은 LoRA 가중치에 대해서만 필요합니다. QLoRA 연구진은 신경망 가중치가 대체로 정규 분포를 따른다는 점에 맞춰 설계한 4비트 형식 NF4(NormalFloat)를 사용해 정밀도 손실을 줄였고, 650억 개 매개변수 모델을 48GB GPU 한 장으로 미세 조정할 수 있음을 보였습니다. 앞에서 계산한 매개변수당 16바이트를 적용하면, 전체 미세 조정에는 가중치와 최적화 상태만으로 약 1TB의 메모리가 필요한 규모입니다.
지식 증류(Knowledge Distillation)
양자화가 숫자를 줄이는 방법이라면, 지식 증류(Knowledge Distillation)는 모델 자체를 작게 만드는 방법입니다. 큰 모델인 교사(Teacher)의 출력을 정답으로 삼아, 작은 모델인 학생(Student)이 그 출력을 흉내 내도록 학습합니다. 2015년 힌턴(Geoffrey Hinton) 연구진이 체계화한 방법입니다.
핵심은 교사의 확률 분포 전체를 배운다는 점입니다. 정답 레이블은 “고양이”에 1, 나머지에 0을 주지만, 교사 모델은 “고양이” 0.85, “호랑이” 0.10, “자동차” 0.0001처럼 정답이 아닌 항목에도 확률을 나눠 줍니다. “호랑이”가 “자동차”보다 고양이와 훨씬 비슷하다는 정보가 이 작은 확률들에 담겨 있으며, 학생은 정답만 볼 때보다 훨씬 많은 것을 배웁니다. 이 정보를 더 잘 드러내기 위해 소프트맥스에 온도를 높여 분포를 완만하게 만든 뒤 학습합니다.
2019년에 발표된 DistilBERT는 BERT를 증류해 크기를 40% 줄이고 속도를 60% 높이면서, 언어 이해 성능의 97%를 유지했다고 보고했습니다. 대규모 언어 모델에서는 큰 모델이 생성한 답변과 풀이 과정을 작은 모델의 학습 데이터로 사용하는 방식의 증류가 널리 쓰입니다.
KV 캐시와 어텐션 최적화
대규모 언어 모델에서 언급했듯, 언어 모델은 토큰을 하나씩 생성할 때 앞에서 계산한 키와 값을 저장해 두고 재사용합니다. 이 KV 캐시(KV Cache) 덕분에 매번 모든 토큰을 다시 계산하지 않아도 되지만, 대신 캐시 자체가 큰 메모리를 차지합니다.
KV 캐시의 크기는 층마다, 토큰마다 키와 값 벡터를 하나씩 저장하므로 다음과 같이 계산됩니다.
\[\text{KV 캐시 크기} = 2 \times \text{층 수} \times \text{토큰 수} \times d_{\text{model}} \times \text{바이트 수}\]층이 32개이고 \(d_{\text{model}} = 4096\)인 모델을 16비트로 실행하면, 토큰 하나당 \(2 \times 32 \times 4096 \times 2\)바이트, 약 0.5MB가 필요합니다. 4,096개 토큰 길이의 문장 하나면 약 2.1GB이고, 여러 사용자의 요청을 동시에 처리하면 사용자 수만큼 늘어납니다. 긴 문서를 다룰수록 가중치보다 KV 캐시가 더 큰 메모리를 차지하기도 합니다.
이를 줄이기 위해 멀티 헤드 어텐션의 구조를 바꾸는 방법이 등장했습니다. 원래는 헤드마다 쿼리, 키, 값을 모두 따로 가지지만, 멀티 쿼리 어텐션(Multi-Query Attention)은 모든 헤드가 키와 값을 하나만 공유하게 합니다. 성능이 조금 떨어지는 대신 KV 캐시가 헤드 수만큼 줄어듭니다. 그룹 쿼리 어텐션(Grouped-Query Attention)은 그 중간으로, 헤드를 몇 개의 그룹으로 나누어 그룹마다 키와 값을 공유합니다. 헤드 32개를 8개 그룹으로 나누면 KV 캐시가 4분의 1이 되며, 성능은 원래와 거의 같아 최근 모델 대부분이 이 방식을 사용합니다.
어텐션 계산 자체를 빠르게 하는 방법도 있습니다. 2022년 다오(Tri Dao) 연구진이 제안한 FlashAttention은 어텐션의 수식은 그대로 두고 계산 순서만 바꿉니다. 큰 점수 행렬 전체를 GPU의 느린 메모리에 썼다가 다시 읽는 대신, 작은 조각으로 나누어 빠른 내부 메모리 안에서 소프트맥스와 가중합까지 한 번에 처리합니다. 결과는 원래 어텐션과 정확히 같으면서 속도는 몇 배 빨라지고 메모리도 크게 줄어듭니다.
여러 사용자의 요청을 함께 처리할 때는 KV 캐시를 담을 메모리를 효율적으로 나누는 것도 중요합니다. 2023년 발표된 PagedAttention은 운영체제가 메모리를 작은 페이지 단위로 관리하듯, KV 캐시를 고정된 크기의 블록으로 나누어 필요한 만큼만 할당합니다. 요청마다 최대 길이만큼 메모리를 미리 잡아 두는 낭비가 사라져, 같은 GPU로 훨씬 많은 요청을 동시에 처리할 수 있습니다.
추측 디코딩(Speculative Decoding)
언어 모델은 토큰을 하나씩 차례대로 생성하므로, 100개의 토큰을 만들려면 큰 모델을 100번 실행해야 합니다. 그런데 앞에서 설명했듯 이때 GPU는 연산 능력의 대부분을 놀리고 있습니다. 토큰 하나를 계산하든 여러 개를 한꺼번에 계산하든, 가중치를 메모리에서 읽어 오는 시간은 거의 같기 때문입니다.
추측 디코딩(Speculative Decoding)은 이 점을 이용합니다. 작고 빠른 초안 모델(Draft Model)이 먼저 다음 토큰 몇 개를 추측해서 만들고, 큰 모델은 그 토큰들을 한 번의 실행으로 동시에 검증합니다. 큰 모델의 예측과 일치하는 토큰은 그대로 받아들이고, 처음으로 어긋나는 위치에서는 큰 모델의 선택으로 바꾼 뒤 다시 추측을 시작합니다.
예를 들어 초안 모델이 “오늘 날씨가 정말 좋다”의 네 토큰을 추측했고, 큰 모델이 앞의 세 토큰에 동의했다면, 큰 모델을 한 번 실행해 세 토큰과 큰 모델이 고른 네 번째 토큰까지 네 개를 얻게 됩니다. 조사나 어미, 자주 쓰이는 표현처럼 예측하기 쉬운 토큰은 작은 모델도 잘 맞히므로, 큰 모델의 실행 횟수가 크게 줄어듭니다.
중요한 점은 이 방법이 결과를 근사하지 않는다는 것입니다. 검증 과정에서 확률에 따라 토큰을 받아들이거나 거절하는 규칙을 적절히 설계하면, 최종 출력의 확률 분포가 큰 모델만으로 생성했을 때와 수학적으로 같아집니다. 품질을 전혀 잃지 않고 속도를 2~3배 높일 수 있어 널리 사용됩니다.
효율화 방법 정리
| 방법 | 줄이는 비용 | 원리 | 대가 |
|---|---|---|---|
| 어댑터 | 미세 조정 메모리, 저장 공간 | 층 사이에 작은 신경망 추가 | 추론 계산 소폭 증가 |
| 프롬프트 튜닝 | 미세 조정 메모리, 저장 공간 | 학습 가능한 가상 토큰 추가 | 작은 모델에서는 성능 부족 |
| LoRA | 미세 조정 메모리, 저장 공간 | 가중치 변화를 낮은 계수 행렬로 표현 | 계수에 따라 표현력 제한 |
| 양자화 | 추론 메모리, 속도 | 가중치를 적은 비트로 저장 | 이상치 처리, 정밀도 손실 |
| QLoRA | 미세 조정 메모리 | 4비트 원본 모델 위에 LoRA 학습 | 복원 계산으로 학습 속도 저하 |
| 지식 증류 | 모델 크기 전체 | 큰 모델의 출력 분포를 작은 모델이 학습 | 증류 학습 비용, 일부 성능 손실 |
| 그룹 쿼리 어텐션 | KV 캐시 메모리 | 헤드 그룹이 키와 값을 공유 | 모델 구조 변경 필요 |
| FlashAttention | 어텐션 속도, 메모리 | 계산 순서를 바꿔 메모리 접근 감소 | 없음(결과 동일) |
| 추측 디코딩 | 생성 속도 | 작은 모델의 추측을 큰 모델이 일괄 검증 | 초안 모델 추가 필요 |
모델을 효율적으로 만드는 방법들은 무엇을 아낄 것인가에 따라 갈립니다. 미세 조정에서는 학습할 매개변수를 줄이고, 추론에서는 숫자의 비트 수와 메모리 접근을 줄이며, 생성에서는 큰 모델을 실행하는 횟수를 줄입니다. 이 방법들은 서로 배타적이지 않아, 양자화한 모델에 LoRA를 붙여 학습하고, 그룹 쿼리 어텐션과 FlashAttention으로 실행하며, 추측 디코딩으로 생성하는 식으로 함께 사용됩니다.
대규모 언어 모델의 능력이 규모에서 나왔다면, 효율화는 그 능력을 제한된 자원에서도 사용할 수 있게 만드는 일입니다. 거대한 데이터센터에서만 실행할 수 있던 규모의 모델이 일반 그래픽카드나 노트북에서도 돌아가게 된 것은 이 강좌에서 다룬 방법들이 함께 쌓인 결과입니다.
- Tip : 효율화 기법을 적용할 때는 반드시 같은 평가 데이터로 전후 성능을 비교해야 합니다. 양자화나 증류의 성능 저하는 평균 점수에서는 작아 보여도, 수학 계산이나 드문 언어처럼 특정 영역에서 크게 나타나는 경우가 있기 때문입니다.
- Writer by : 윤대희
댓글 남기기