AEGLOSLAB ChatGPT 같은 AI를 만들고 서비스하기

제3부 사후학습·정렬(Post-training/Alignment)

3.8 PEFT — LoRA·QLoRA·어댑터

7B 모델 하나를 손안의 데이터로 미세조정하려 한다. 가중치가 bf16으로 14 GB니까 80 GB GPU 한 장이면 충분하리라 보고 학습을 켠다. 그러자 프레임워크가 112 GB를 요구하며 그 자리에서 종료된다. 추론이면 14 GB로 끝났을 모델이 학습에서는 여덟 배를 달라고 한다. 이 여덟 배가 정확히 어디서 오는지, 그리고 그 대부분을 어떻게 60 MB로 지워 버리는지 — 그것이 이 장이다.

미세조정이라 하면 보통 모든 가중치를 경사하강으로 갱신하는 전체 미세조정(full fine-tuning)을 떠올린다. SFT(3.2)도, 보상모델 학습도, DPO(3.5)도 원리상 이 방식으로 할 수 있다. 문제는 그것이 대부분의 팀에게 감당이 안 된다는 것이다. 매개변수 효율적 미세조정(Parameter-Efficient Fine-Tuning, PEFT)은 이 장벽을 정면으로 넘지 않고 우회한다 — 원래 가중치는 거의 다 동결하고 아주 적은 수의 새 파라미터만 학습한다. 이 장은 사실상 표준이 된 LoRA와 그 양자화 결합 QLoRA를 계산과 서빙까지 다루고, 어댑터·prefix/prompt tuning·IA3·DoRA가 왜 대체됐고 언제 다시 꺼내는지를 본다.

3.8.1 전체 미세조정의 첫 비용 — 파라미터당 16바이트

앞의 112 GB가 어디서 왔는지부터 명시하자. 가중치 하나를 경사하강으로 학습하려면 그 가중치만 들고 있어서는 안 된다. 그래디언트가 필요하고, 옵티마이저의 상태가 필요하다. 요즘 표준인 혼합정밀도(2.6) + Adam(옵티마이저 상태의 배경은 1.3)으로 학습할 때, 파라미터 하나당 메모리 회계는 이렇게 쌓인다.

항목정밀도바이트/파라미터왜 필요한가
가중치 사본(순전파·역전파용)bf162실제 계산에 쓰는 저정밀 사본
그래디언트bf162역전파가 채운다
fp32 마스터 가중치fp324미세한 갱신 누적용 고정밀 원본
Adam 1차 모멘트 $m$fp324그래디언트의 지수이동평균
Adam 2차 모멘트 $v$fp324그래디언트 제곱의 지수이동평균
합계≈ 16

Adam이 파라미터마다 $m$과 $v$ 두 개의 상태를 fp32로 들고 다닌다는 점이 회계의 절반을 차지한다. 왜 fp32인가 — bf16으로 모멘트를 누적하면 작은 갱신이 반올림에 먹혀 학습이 조용히 망가지기 때문이고, 이래서 마스터 가중치도 fp32로 따로 둔다(2.6). 이 다섯 줄을 합치면

$$M_{\text{학습}} \approx 16\ \text{bytes} \times N_{\text{params}} \;+\; M_{\text{활성값}}$$

7B이면 $16 \times 7\times10^9 = 112\ \text{GB}$다 — 활성값을 아직 더하기도 전이다. 추론은 가중치만 읽으면 되니 14 GB로 끝나는데, 학습은 같은 모델에 8배를 물린다. 결정적인 관찰은 이 16바이트 중 가중치 사본 2바이트를 뺀 14바이트가 오직 "이 파라미터가 학습된다"는 사실 때문에 붙는다는 것이다. 그래디언트도, 마스터도, Adam 상태도, 동결된 파라미터에는 아예 없다. 그러니 학습되는 파라미터를 전체의 1%로 줄이면 이 14배가 사실상 전부 사라진다. 이것이 PEFT의 첫 번째이자 가장 큰 수단이다.

여기에 활성값이 또 추가된다는 점은 정직하게 밝혀야 한다. 활성값은 배치 크기·시퀀스 길이·층 수에 비례하고, 역전파를 위해 순전파의 중간 결과를 유지하고 있어야 해서 종종 옵티마이저 상태만큼 커진다. 그래디언트 체크포인팅(2.6)으로 이것을 재계산과 맞바꿔 줄이는데, 뒤에서 보겠지만 PEFT는 이 활성값을 거의 줄여 주지 못한다 — 순전파는 여전히 베이스 전체를 통과하기 때문이다. "PEFT면 학습 메모리가 다 공짜"라는 흔한 기대가 깨지는 첫 지점이 여기다.

3.8.2 나머지 두 비용 — 저장, 다태스크 서빙, 그리고 한 줄

학습 메모리는 세 비용 중 하나일 뿐이다. 둘째는 저장이다. 전체 FT의 산출물은 베이스와 똑같은 크기의 새 모델 한 벌이다. 7B(bf16 14 GB)를 태스크 10개에 맞추면 140 GB의 체크포인트가 쌓이고, 여기에 실험·버전·롤백 대상을 곱하면 저장·전송 비용이 눈덩이가 된다(모델 라이프사이클은 8.3). 셋째는 서빙이다. 전체 FT 모델은 각자 GPU 메모리 전체를 차지하므로, 태스크·고객마다 다른 모델이면 원칙적으로 테넌트 수만큼 GPU가 필요하다 — 멀티테넌시(5.5·7.2)에서 이것은 비용을 곱셈으로 키우는 치명적 문제다.

세 비용의 근원은 하나다. "모든 파라미터를 바꾼다"는 전제. PEFT는 이 전제를 이렇게 뒤집는다 — 큰 사전학습 모델은 이미 대부분의 지식을 갖고 있고, 특정 태스크로의 적응은 원본에 추가하는 "작은 수정"으로 충분할 때가 많다. 그렇다면 그 작은 수정만 학습하고 나머지는 동결하자. 그러면 학습 메모리의 14배가 사라지고(첫째), 산출물이 베이스가 아니라 수십 MB 크기의 수정분 하나가 되며(둘째), 서빙 때 베이스를 공유하고 수정분만 갈아 끼울 수 있다(셋째). 하나의 발상이 세 비용을 동시에 낮춘다.

이 발상이 실제로 통하느냐는 순전히 경험적 질문이다. 놀랍게도 많은 태스크에서 "작은 수정"만으로 전체 FT에 근접한다는 것이 반복 관찰됐다 — 다만 이 "근접"이 언제 벌어지고 언제 붕괴하는지는 이 장 후반에서 정직하게 따진다. 왜 작은 수정으로 충분한지에 대한 이론적 힌트는 다음 절, LoRA의 저랭크 가정에서 나온다.

3.8.3 LoRA의 가정 — 적응은 저랭크다

LoRA(Low-Rank Adaptation)의 발상은 한 문장이다. 미세조정이 만드는 가중치 변화량 $\Delta W$ 전체를 학습하지 말고, 두 개의 얇은 행렬의 곱으로 근사하자.

전체 FT는 각 가중치 $W_0 \in \mathbb{R}^{d\times k}$를 $W_0 + \Delta W$로 바꾼다. LoRA의 가설은 이 $\Delta W$가 "본질적으로 저랭크(low intrinsic rank)"라는 것 — 즉 $\Delta W$의 정보가 $\min(d,k)$보다 훨씬 작은 랭크 $r$의 부분공간에 거의 담긴다는 관찰이다. 근거는 사전학습된 대형 모델을 특정 태스크로 옮길 때, 그 태스크를 푸는 데 필요한 "본질적 차원(intrinsic dimension)"이 놀랄 만큼 작다는 선행 연구다(Aghajanyan 등이 무작위 저차원 부분공간에서만 파라미터를 움직여도 태스크가 풀린다는 것을 보였고, Hu 등의 LoRA 논문이 이를 $\Delta W$에 대한 구조 가정으로 옮겼다). 이것이 사실이라면 $\Delta W$를 랭크 $r$로 강제 분해해도 잃는 것이 작다.

$$\Delta W \approx B A, \qquad B \in \mathbb{R}^{d\times r},\quad A \in \mathbb{R}^{r\times k},\quad r \ll \min(d,k)$$

$A$는 $k$차원을 $r$차원으로 압축해 병목을 만들고, $B$는 그 $r$차원을 다시 $d$차원으로 펼친다. 두 행렬을 곱하면 $d\times k$ 모양이 복원되지만, 자유도는 원본의 $dk$가 아니라 $r(d+k)$뿐이다. 왜 이것이 극적인 절약인지는 랭크의 정의를 떠올리면 분명하다 — $BA$의 랭크는 아무리 커도 $r$이므로, LoRA는 $\Delta W$가 살 수 있는 부분공간을 $r$개의 방향으로 제한한다. 태스크가 정말 몇 개의 방향만 필요로 한다면 이 제한은 비용이 아니라 정규화(regularization)로 작동해 과적합을 오히려 줄인다. 반대로 태스크가 많은 방향을 요구하면 $r$이 병목이 되어 품질이 저하된다. 그래서 $r$은 "표현 예산"이고, 이 장 뒤에서 그 예산을 어떻게 설정하는지가 반복해 돌아온다.

3.8.4 순전파·초기화·스케일 — B=0과 α/r은 왜 그렇게 정하나

LoRA는 원 가중치 $W_0$를 동결하고, 그 옆에 $BA$ 경로를 병렬로 더한다. 한 선형 층의 순전파는

$$h = W_0 x + \Delta W x = W_0 x + \frac{\alpha}{r}\, B A\, x$$

$W_0$는 그래디언트도 옵티마이저 상태도 갖지 않고, $A$와 $B$만 학습된다. $\alpha$는 스케일 하이퍼파라미터로, $BA$의 실제 기여를 $\alpha/r$로 조절한다.

입력 x (차원 k) 동결 W₀ (d×k · 학습 안 함) A (r×k) 학습됨 init ~ N(0,σ²) B (d×r) 학습됨 init = 0 × α/r (스케일) + 출력 h (차원 d)

왜 $B=0$, $A$는 랜덤인가. LoRA는 $A$를 작은 가우시안으로, $B$를 정확히 0으로 초기화한다. 이유는 그래디언트를 직접 따라가면 나온다. 손실 $L$에 대해 두 행렬의 그래디언트는 ($s=\alpha/r$로 두면)

$$\frac{\partial L}{\partial B} = s\,\frac{\partial L}{\partial h}\,(Ax)^\top, \qquad \frac{\partial L}{\partial A} = s\, B^\top\,\frac{\partial L}{\partial h}\,x^\top$$

첫 스텝에 $B=0$이면 $\partial L/\partial A = s\cdot 0^\top \cdot(\cdots) = 0$이라 $A$는 움직이지 않지만, $\partial L/\partial B = s\,(\partial L/\partial h)(Ax)^\top$은 $A$가 랜덤이라 0이 아니어서 $B$가 0에서 떠오른다. 동시에 학습 시작 시점의 $BA = 0$이므로 $\Delta W = 0$ — 모델이 베이스와 한 치도 다르지 않은 상태에서 출발한다. 파인튜닝이 사전학습을 망가뜨리며 시작하지 않고, 필요한 수정분만 0에서부터 쌓는다. 만약 둘 다 0으로 두면 두 그래디언트가 모두 0이라 영원히 0에 갇히고, 둘 다 랜덤으로 두면 시작부터 $\Delta W \neq 0$이라 베이스가 흐트러진 채로 학습을 시작한다. 정확히 한쪽만 0으로 두는 것이 "베이스에서 출발해 수정분을 누적"을 가능케 하는 유일한 선택이다.

왜 $\alpha/r$로 나누나. 이 스케일의 목적은 랭크 $r$을 바꿔도 학습률을 매번 다시 설정하지 않게 하는 것이다. $BA$의 한 원소는 $(BA)_{mn} = \sum_{i=1}^{r} B_{mi}A_{in}$으로, $r$개 항의 합이다. 원소들이 대략 독립이라고 보면 합의 분산이 항 수 $r$에 비례해 커지므로 $BA$의 전형적 크기가 $\sqrt{r}$ 규모로 자란다. 스케일이 없으면 $r$을 8에서 64로 높일 때 $\Delta W$의 크기 전체가 커져 유효 학습률이 불안정해지고, 그때마다 학습률을 다시 튜닝해야 한다. $r$로 나눠 두면 이 성장을 상쇄해 $r$을 자유롭게 쓸어 볼 수 있다. 엄밀히는 분산 논증이 $\sqrt{r}$을 가리키는데 원 LoRA는 $r$로 나누므로 큰 $r$에서 과도하게 억제되는 경향이 있고, 이래서 후속 rsLoRA는 $\alpha/\sqrt{r}$로 스케일해 큰 랭크의 학습 안정성을 회복한다. 실무에서 $\alpha=r$(스케일 1)이나 $\alpha=2r$(스케일 2)로 두는 관행이 흔하지만 이것은 물리 법칙이 아니라 경험칙이고, 무엇보다 $\alpha$만 키우고 $r$을 그대로 두면 사실상 유효 스케일을 높이는 것이라 학습률을 키운 것과 비슷하게 작동한다는 점을 기억해야 한다.

이 "병렬로 더하는 저랭크 잔차"라는 구조가 뒤에서 볼 서빙 유연성의 근원이다. $W_0$를 변경하지 않으므로, 추론 때 $BA$를 $W_0$에 구워 넣어도 되고(병합) 옆에서 따로 계산해 더해도 된다(동적). 어느 쪽이든 $W_0$는 온전하다.

3.8.5 어느 층에 적용하나

LoRA는 임의의 선형 층에 적용할 수 있다. 트랜스포머(1.4)의 후보는 크게 어텐션의 네 투영 $W^Q, W^K, W^V, W^O$와 FFN의 투영들(SwiGLU 계열이면 gate·up·down, 1.5)이다. 어느 것을 고르느냐가 학습 파라미터 수와 품질을 함께 정한다.

원 LoRA 논문은 어텐션의 $W^Q, W^V$에만 적용해도 상당한 효과를 봤다고 보고했다. 그러나 이후 실무의 대체적 결론은 "랭크를 크게 키우기보다, 더 많은 층에 낮은 랭크로 붙이는 편이 대체로 낫다"는 쪽으로 굳었다. 특히 FFN을 포함한 모든 선형 층에 적용하는 설정이 흔한 기본값이 됐고, QLoRA 논문도 이 설정을 권했다. 직관은 이렇다 — FFN은 트랜스포머 파라미터의 3분의 2 안팎을 차지하는데, 태스크 적응이 필요로 하는 표현 변화의 상당 부분이 거기서 일어난다. 어텐션 투영만 변경하면 모델이 "무엇에 주목할지"는 바꿀 수 있어도 "그 정보로 무엇을 계산할지"는 거의 못 바꾼다.

대상층학습 파라미터어울리는 상황
$W^Q, W^V$만최소자원 극빈·단순 태스크, 원 논문 설정
어텐션 4투영 전부중간균형점
모든 선형 층(어텐션+FFN)큼(그래도 전체의 1% 안팎)현대 실무의 흔한 기본값, 품질에 유리

최적값은 모델·태스크·데이터에 달렸으니 위 표는 강제가 아니라 출발점이다. 내 권고는 자원이 허락하는 한 "모든 선형 층 + 낮은 랭크"에서 시작해 검증셋으로 조정하는 것이다 — 랭크를 높이는 것보다 대상층을 넓히는 편이 같은 파라미터 예산에서 대체로 더 안전하기 때문이다. 다만 주의할 함정 하나. target_modules에 넣는 층 이름은 모델 아키텍처마다 다르다(예: q_proj·gate_proj는 특정 구현의 이름이고 다른 모델에서는 다르게 부른다). 이름을 잘못 넣으면 아무 층에도 어댑터가 적용되지 않은 채로 "학습이 되는 것처럼" 보이다가 성능이 안 오르는 조용한 실패가 난다.

3.8.6 수계산 — 학습 파라미터와 메모리 절감

숫자로 개략을 파악하자. 모두 개략값이고 설명용이다.

한 행렬. $d=k=4096$인 정사각 가중치에 랭크 $r=8$을 붙인다.

$$\text{전체 FT}: dk = 4096^2 = 16{,}777{,}216 \approx 16.8\text{M}$$ $$\text{LoRA}: r(d+k) = 8\times 8192 = 65{,}536 \approx 65.5\text{K}$$

비율은 $\dfrac{r(d+k)}{dk} = \dfrac{2r}{d} = \dfrac{16}{4096} \approx 0.39\%$. 이 하나의 행렬에서 LoRA는 전체 FT의 0.4%만 학습한다. 이 식이 알려 주는 것이 하나 더 있다 — 비율이 $2r/d$이므로, 모델이 넓을수록($d$가 클수록) 같은 랭크에서 LoRA의 상대적 절약이 커진다. 큰 모델일수록 PEFT가 더 매력적인 이유다.

7B 전체, Q·V만, $r=8$. $d=4096$, 층 수 $L=32$, 각 $W^Q, W^V$가 $4096\times4096$.

  • 행렬 하나: $2 \cdot 4096 \cdot 8 = 65{,}536$
  • 층당 (Q, V 둘): $2 \cdot 65{,}536 = 131{,}072$
  • 전체 32층: $32 \cdot 131{,}072 = 4{,}194{,}304 \approx 4.19\text{M}$

학습 파라미터 $\approx 4.2\text{M} / 7\text{B} \approx 0.06\%$.

7B 전체, 모든 선형 층, $r=16$. 이번엔 FFN까지 넣는다(어텐션 $d=4096$, FFN 중간차원 $\approx 11008$).

  • 어텐션 q,k,v,o: $4 \cdot 16 \cdot (4096+4096) = 524{,}288$
  • FFN gate, up: $2 \cdot 16 \cdot (4096+11008) = 483{,}328$
  • FFN down: $1 \cdot 16 \cdot (11008+4096) = 241{,}664$
  • 층당 합: $= 1{,}249{,}280$
  • 전체 32층: $32 \cdot 1{,}249{,}280 \approx 40.0\text{M}$

학습 파라미터 $\approx 40\text{M} / 7\text{B} \approx 0.57\%$. 대상층을 대폭 넓히고 랭크도 두 배로 높였는데도 여전히 전체의 1% 아래다. 디스크에 저장하면 이 어댑터는 $40\text{M} \times 2\ \text{B(bf16)} \approx 80\ \text{MB}$ — 14 GB 베이스 옆에 붙는 부록 한 장이다.

메모리 절감(모델·옵티마이저). 3.8.1의 회계를 적용한다. Q·V에만 적용한 4.2M 기준으로:

항목전체 FT (7B)LoRA (7B, 학습 4.2M)
베이스 가중치bf16 14 GB (학습됨)bf16 14 GB (동결)
그래디언트14 GB$4.2\text{M}\times 2 \approx 8\ \text{MB}$
옵티마이저+마스터(Adam)84 GB$4.2\text{M}\times 12 \approx 50\ \text{MB}$
모델·옵티마이저 소계≈ 112 GB≈ 14 GB + 60 MB

전체 FT의 부담을 키우던 98 GB의 그래디언트·옵티마이저 상태 전체가 60 MB로 붕괴했다(모든 선형 층 40M을 추가해도 이 항은 560 MB 정도로, 여전히 무의미하게 작다). 남은 병목은 동결 베이스 14 GB와 활성값이다. 그런데 활성값은 순전파가 베이스 전체를 통과하는 이상 LoRA로 줄지 않는다 — 그래서 배치·시퀀스가 크면 그래디언트 체크포인팅(2.6)이 여전히 필요하고, 이것이 "LoRA면 활성값도 공짜"라는 오해가 깨지는 지점이다. 어쨌든 그래디언트·옵티마이저가 사라졌으니 다음 수순은 자명하다 — 남은 14 GB, 즉 동결 베이스마저 줄이자. 그것이 QLoRA다.

3.8.7 QLoRA — 4비트 베이스 위에 LoRA를 추가하다

LoRA가 옵티마이저·그래디언트 메모리를 없앴다면, QLoRA(Dettmers 등)는 남은 병목인 동결 베이스 자체를 4비트로 양자화한다. 대표 주장은 단일 GPU에서 65B 모델을 파인튜닝하면서 16비트 전체 FT에 필적하는 품질을 얻었다는 것인데, 이 "필적"의 조건은 3.8.8에서 정직하게 따진다. 핵심 아이디어는 "저장·전달은 4비트, 계산은 고정밀"이라는 분리이고, 세 부품이 협력해 이것을 실현한다.

메모리에 상주 (작게) 순전파·역전파 (고정밀) 동결 베이스 가중치 4비트 NF4 (~0.5 B/param) 이중 양자화된 양자화 상수 필요할 때 블록 단위 bf16으로 역양자화 bf16 행렬곱 W₀x + (α/r)·B·A·x (LoRA, bf16 학습) 출력 h Paged optimizer (메모리 스파이크 시 CPU로 페이징)

① 4비트 NormalFloat(NF4) 베이스. 베이스 가중치를 4비트로 저장하되, 아무 4비트 격자가 아니라 정규분포에 정보이론적으로 맞춘 격자를 쓴다. 신경망 가중치는 대략 평균 0의 정규분포를 따르는데, 균등 격자(예: 값 범위를 16등분)를 쓰면 가중치가 몰려 있는 0 근처를 성기게, 거의 없는 꼬리를 조밀하게 분할하는 낭비가 생긴다. NF4는 표준정규분포의 분위수(quantile)로 격자점을 설정해 각 4비트 코드가 동일한 확률질량을 담게 한다 — 값이 몰린 곳에 코드를 더 많이 배치하는 셈이다. 양자화는 블록 단위로 하고 블록마다 스케일(absmax)을 따로 둬, 이상치 한 개가 블록 전체의 해상도를 소모하지 않게 한다. NF4·INT4의 상세와 GPTQ·AWQ 같은 추론용 양자화와의 차이는 5.2에서 다룬다.

② 이중 양자화(Double Quantization). 블록마다 두는 스케일 상수(보통 fp32)도 블록이 많아지면 무시 못 할 오버헤드가 된다. QLoRA는 이 양자화 상수 자체를 다시 양자화한다 — 스케일들을 모아 8비트로 압축해 저장하고, 그 위에 다시 한 겹의 스케일만 fp32로 둔다. 다음 절 계산에서 보겠지만 이것으로 파라미터당 오버헤드가 약 0.5비트에서 0.13비트 수준으로 내려간다.

③ Paged optimizer. 그래디언트 체크포인팅(2.6)을 쓰면 특정 순간 메모리가 일시적으로 급등해(spike) OOM이 날 수 있다. QLoRA는 NVIDIA 통합 메모리(unified memory)를 이용해 옵티마이저 상태를 필요 시 CPU RAM으로 페이징했다가 되불러, 이 스파이크에서 죽지 않게 한다. OS의 가상메모리 페이징과 똑같은 발상을 옵티마이저 상태에 적용한 것이다.

3.8.8 왜 4비트 베이스인데 품질이 유지되나

"4비트로 뭉갠 모델을 파인튜닝하면 형편없지 않나?"라는 의심은 정당하다. QLoRA가 그럭저럭 되는 데는 세 가지가 겹친다. 첫째, 양자화되는 것은 동결 베이스뿐이고 LoRA 어댑터 $A, B$는 더 높은 정밀도(bf16)로 학습된다. 학습 신호는 고정밀 어댑터로 흐르므로, 베이스의 4비트 반올림 오차를 어댑터가 흡수·보정할 여지가 있다 — 어댑터가 사실상 "양자화 오차를 포함한 잔차"를 학습한다고 봐도 된다. 둘째, 저장만 4비트지 계산은 4비트로 하지 않는다. 순전파 때 필요한 블록을 bf16으로 역양자화한 뒤 행렬곱하므로, 정밀도 손실은 "저장 반올림" 한 겹에 국한된다. 셋째, NF4가 가중치 분포에 잘 맞아 균등 4비트보다 반올림 오차 자체가 작다.

그렇다고 "QLoRA = 전체 FT 동급"을 무조건 수용해서는 안 된다. 이것은 특정 벤치마크·태스크에서의 연구 주장이고, 조건을 바꾸면 결이 달라진다. 4비트 양자화의 손실은 0이 아니어서, 정밀한 수치·회귀·저자원 언어처럼 작은 오차가 누적되는 태스크에서는 격차가 벌어질 수 있다. 어댑터가 큰 표현 변화를 요구하는 태스크나 매우 낮은 랭크에서도 마찬가지다. 그리고 벤치마크 자체가 오염·과적합될 수 있다(4.1). 정확한 요약은 "메모리를 8배 아끼면서 대개 근접하지만, 동급을 보장하지는 않는다"이다. 메모리가 장벽일 때 QLoRA는 거의 언제나 옳은 선택이지만, 메모리에 여유가 있다면 bf16 LoRA가 품질·속도 양쪽에서 조금 앞선다.

3.8.9 수계산 — QLoRA 메모리·이중 양자화·속도

먼저 이중 양자화가 실제로 얼마를 아끼는지 비트 단위로 따라가자. NF4 베이스는 파라미터당 4비트다. 블록 크기를 64로 두면 블록마다 fp32 스케일 하나가 붙으니, 스케일 오버헤드는 $32\ \text{bit} / 64\ \text{param} = 0.5\ \text{bit/param}$다. 그래서 순진하게는 파라미터당 $4 + 0.5 = 4.5$비트가 든다.

이중 양자화는 이 스케일들을 다시 압축한다. fp32 스케일을 8비트로 양자화하고, 스케일 256개마다 두 번째 층 fp32 스케일 하나를 둔다. 파라미터당 오버헤드는

$$\underbrace{\frac{8}{64}}_{\text{8비트 스케일}} + \underbrace{\frac{32}{64\times 256}}_{\text{2차 스케일}} = 0.125 + 0.002 \approx 0.127\ \text{bit/param}$$

즉 스케일 오버헤드가 $0.5 \to 0.127$비트로 줄어 파라미터당 약 0.37비트를 아낀다. 65B에 곱하면 $0.373 \times 65\times10^9 / 8 \approx 3.0\ \text{GB}$ — 논문이 65B에서 수 GB를 아꼈다는 보고와 맞는다. 최종 유효 정밀도는 파라미터당 약 4.13비트이므로, 7B 베이스는 $4.13 \times 7\times10^9 / 8 \approx 3.6\ \text{GB}$로 앉는다.

이제 세 방식을 나란히 놓으면(7B, 개략값):

방식동결 베이스 저장그래디언트·옵티마이저모델·옵티마이저 소계
전체 FT (bf16)14 GB (학습됨)~98 GB~112 GB
LoRA (bf16 베이스)14 GB~60 MB~14 GB
QLoRA (NF4 베이스)~3.6 GB~60 MB~4 GB

베이스가 14 GB에서 3.6 GB로 내려가면서, 활성값을 추가해도 24 GB급 소비자 GPU 한 장에 7B 파인튜닝이 들어온다. 논문이 48 GB 한 장에서 65B를 파인튜닝한 것이 이 계산의 극단 사례다. QLoRA가 "파인튜닝의 민주화"로 불린 이유가 이 표에 다 있다 — 비용을 메모리 축에서 결정적으로 붕괴시켰다.

무비용은 아니다. QLoRA는 메모리를 벌지만 속도를 조금 지불한다. 매 순전파에서 4비트 블록을 bf16으로 역양자화하는 오버헤드가 추가되어, 같은 하드웨어에서 bf16 LoRA보다 학습 처리량이 낮을 수 있다. 그래서 갈림은 대략 이렇다 — 메모리가 장벽이면 QLoRA, 속도가 장벽이고 메모리에 여유가 있으면 bf16 LoRA. 정확한 손익은 하드웨어와 커널 구현에 따라 달라지니 실측이 필요하다.

3.8.10 LoRA만이 아니다 — 어댑터·prefix/prompt·IA3·DoRA

LoRA가 유일한 PEFT는 아니다. 계열을 알아야 "왜 LoRA가 이겼는지"와 "언제 다른 걸 꺼내는지"가 보인다. 크게 세 부류다 — 가중치에 저랭크 항을 더하는 계열(LoRA·DoRA), 층 사이에 모듈을 삽입하는 계열(어댑터), 가중치는 안 변경하고 입력·활성을 조정하는 계열(prefix/prompt·IA3).

PEFT 계열 가중치 재매개변수화 (병합 가능) 모듈 삽입 (직렬·병합 불가) 입력·활성 조정 LoRA ΔW=BA DoRA 크기·방향 분해 어댑터 병목 MLP 직렬 prefix tuning 층마다 가상 KV prompt tuning 입력 soft prompt IA3 K·V·FFN 활성 스케일

어댑터(Adapter, Houlsby 2019)는 최초의 대표적 PEFT다. 각 트랜스포머 서브층 뒤에 작은 병목 MLP(차원을 내려찍고 → 비선형 → 다시 높이고 + 잔차)를 직렬로 삽입한다. 잘 작동하지만 두 약점이 치명적이었다. 직렬이라 추론 시 계산 경로가 길어져 지연이 추가되고, LoRA처럼 원 가중치에 병합해 없앨 수가 없다 — 어댑터는 층 사이에 위치한 별도의 연산이라 어디에도 흡수되지 않는다. LoRA가 어댑터를 대체한 결정적 이유가 바로 이 "병합 가능성"의 차이다.

prefix tuning / prompt tuning은 가중치를 전혀 안 바꾸고, 학습 가능한 "가상 토큰(soft prompt)"을 추가한다. prompt tuning은 입력 임베딩 앞에만, prefix tuning은 모든 어텐션 층의 키·값 앞에 학습된 벡터를 붙인다. 파라미터가 극소이고 하나의 베이스에 프롬프트만 교체하기 쉽다는 것이 장점이다. 대신 가상 토큰이 컨텍스트 길이를 소모한다 — 붙인 토큰 수만큼 KV 캐시가 커지고(5.1), 그만큼 실제 프롬프트에 쓸 창이 줄어든다. 표현력·학습 안정성도 태스크에 따라 LoRA보다 약할 수 있다.

IA3는 어텐션의 키·값과 FFN 중간 활성에 학습된 스케일 벡터를 원소별로 곱한다($\odot$). 파라미터가 prefix보다도 적고 병합에 가깝게 흡수할 수 있다. 대신 활성을 스케일만 하므로 표현 변화 폭이 좁아, 큰 적응이 필요한 태스크에는 한계가 있다.

DoRA(Weight-Decomposed LoRA)는 가중치를 크기(magnitude)와 방향(direction)으로 분해해, 방향에만 LoRA를 적용하고 크기는 따로 학습한다. 전체 FT의 학습 동역학(크기와 방향이 따로 움직이는 패턴)에 더 가까워 같은 파라미터로 LoRA보다 품질이 낫다는 것이 주장이고, 태스크·설정에 따라 실제로 그런 관찰이 보고된다. 대가로 약간의 추가 계산과 복잡도를 지불한다.

방식무엇을 학습파라미터추론 오버헤드병합요지
LoRA저랭크 $\Delta W=BA$소~중없음(병합 시)가능현대 표준, 균형
QLoRALoRA + 4비트 동결 베이스소~중학습 시 역양자화가능(어댑터)최소 메모리 학습
DoRA크기·방향 분해 후 LoRA소~중약간가능품질↑ 주장, 복잡도↑
어댑터병목 MLP(직렬)소~중있음(직렬)불가원조 PEFT, 지연 부담
prefix/prompt가상 토큰극소KV 캐시↑불가초경량, 컨텍스트 소모
IA3활성 스케일 벡터극소거의 없음유사 가능최소 파라미터

특별한 이유가 없으면 LoRA/QLoRA에서 시작하는 것이 안전하다는 것이 내 견해다. 툴링(PEFT 라이브러리·서빙 지원)이 가장 성숙하고, 병합 가능이라는 서빙상의 큰 이점이 있기 때문이다. DoRA는 "LoRA로 품질이 살짝 아쉬울 때" 다음 후보이고, prefix/IA3는 파라미터를 극단적으로 줄여야 하거나 다태스크 프롬프트 스위칭이 핵심일 때 고려한다.

3.8.11 병합 — 추론 오버헤드를 0으로

LoRA의 핵심 수단은 학습만이 아니라 서빙 유연성이다. $\Delta W = \frac{\alpha}{r}BA$가 베이스에 병렬로 더해지는 저랭크 항이라는 구조에서, 서빙 시 두 갈래가 열린다. 첫째는 병합이다. 학습이 끝나면 어댑터를 베이스에 구워 넣을 수 있다.

$$W' = W_0 + \frac{\alpha}{r} B A$$

$W'$은 원 가중치와 모양이 똑같으므로, 병합된 모델은 일반 모델과 완전히 동일하게 서빙된다 — 추론 시 LoRA의 흔적도, 추가 지연도 없다. vLLM·TGI 등 어떤 서빙 프레임워크(5.4)도 그대로 "하나의 모델"로 받는다. 대가는 유연성이다. 한 병합 모델은 곧 한 태스크이고, 태스크가 여럿이면 병합 모델도 여럿이라 다태스크 멀티테넌시의 이점을 잃는다. 그리고 QLoRA로 4비트에서 학습한 경우 병합에 함정이 있다 — 어댑터는 bf16으로 학습됐는데 베이스는 4비트로 압축되어 있으니, bf16 베이스에 병합할지 4비트에 병합할지에 따라 미세한 수치 차가 생긴다. 병합 후에는 반드시 검증셋으로 회귀를 확인해야 한다(8.2).

둘째 갈래는 병합하지 않고 베이스와 어댑터를 분리한 채 순전파 때마다 $W_0 x + \frac{\alpha}{r}B(Ax)$를 계산하는 것이다. 저랭크라 추가 계산은 토큰당 $O(r(d+k))$로, 베이스의 $O(dk)$에 비하면 미미하다. 이 "분리 유지"가 다음 절 멀티테넌시의 문을 연다.

3.8.12 멀티-LoRA 서빙 — 한 베이스, 수백 개의 어댑터

핵심 통찰은 하나다. 베이스는 모든 태스크가 공유하고, 태스크별로 다른 것은 수십 MB 크기의 어댑터뿐이다. 그렇다면 하나의 베이스를 GPU에 한 번만 적재하고 요청마다 해당 어댑터를 적용하면, 한 GPU가 수백~수천 태스크를 서빙할 수 있다.

요청 A(고객1) 공유 베이스 W₀ GPU에 한 번만 상주 (연속 배칭으로 함께 처리) 요청 B(고객2) 요청 C(고객1) 요청 D(고객3) 어댑터 α(고객1) 어댑터 β(고객2) 어댑터 γ(고객3) 한 배치 안에서 요청마다 자기 어댑터의 (α/r)·BA만 추가 계산
멀티-LoRA 서빙 — 베이스는 한 벌 공유, 요청마다 다른 어댑터 적용

경제성을 숫자로 보면 왜 이것이 판을 바꾸는지 분명해진다. 80 GB GPU에 7B 베이스(14 GB)를 높이면 KV 캐시와 여유를 빼고도 어댑터에 쓸 공간이 충분하다. 어댑터 하나가 80 MB라면 60 GB에 이론상 $60\ \text{GB} / 80\ \text{MB} = 750$개가 들어간다. 전체 FT였다면 750개 태스크에 750장의 GPU(혹은 끝없는 모델 스와핑)가 필요했을 것을, LoRA는 한 장으로 감당한다. 물론 이것은 메모리 상한이고 동시에 활성으로 실행되는 어댑터 수는 그보다 작지만, "베이스 공유로 태스크의 한계비용이 급감한다"는 요지는 변하지 않는다.

구현은 두 가지를 풀어야 한다. 첫째는 배치 안 이질성이다. 연속 배칭(5.3)으로 여러 요청을 한 배치에 묶는데 요청마다 어댑터가 다르다. 해법은 베이스 행렬곱 $W_0 X$를 모두 공유해 한 번에 하고, 그 위에 요청별 어댑터의 $BA$만 따로 gather해 더하는 것이다 — S-LoRA·Punica류가 이런 배치된 멀티-LoRA 커널을 제안했고 vLLM 등 주요 서빙 프레임워크가 멀티-LoRA를 지원한다(구체 지원·성능은 버전에 따라 다르니 확인이 필요하다, 5.4). 둘째는 어댑터 스와핑이다. 활성 어댑터가 아주 많으면 전부 GPU에 둘 수 없어, 자주 쓰는 것은 GPU에, 나머지는 CPU/디스크에 두고 필요 시 로드한다 — 어댑터가 수십 MB라 이 스왑이 저렴하다는 것이 핵심이다.

이 구조가 무비용은 아니라는 점은 정직하게 밝혀야 한다. 어댑터들이 서로 다른 랭크나 대상층을 쓰면 배치 커널이 복잡해지고, 활성 어댑터 수가 늘면 gather 오버헤드와 메모리 관리가 새 병목이 된다. "무한 태스크가 공짜"가 아니라 "베이스 공유로 한계비용이 급감"이 정확한 서술이다. 운영을 생각하면 어댑터 규격(랭크·대상층)을 공통으로 정해 두는 편이 배치 효율에 유리하다. 이 그림은 프리픽스 캐시·모델 라우팅과 함께 다루는 서빙 최적화(5.6), API 멀티테넌시·쿼터(7.2)로 이어진다.

3.8.13 언제 프롬프트·RAG·PEFT·전체 FT인가

파인튜닝을 결정하기 전에 물어야 할 순서가 있다. 파인튜닝은 종종 과잉 처방이기 때문이다.

예 아니오 최신·사설 지식 행동·형식 ·톤·도메인 문체 단일~수십 태스크, GPU 제한 베이스 능력 자체를 재편, 대규모 자원 태스크 적응이 필요 프롬프트·few-shot으로 충분한가? 프롬프트 엔지니어링 (6.1) — 학습 0 필요한 것이 지식인가 행동·형식·문체인가? RAG (6.3) — 검색으로 주입 자원·품질 요구 PEFT (LoRA/QLoRA) 전체 미세조정
파인튜닝 전에 — 프롬프트·RAG·PEFT·전체FT 결정

프롬프트나 few-shot으로 되면 파인튜닝하지 마라(6.1). 학습이 0이고 반복이 빠르다. "몇 가지 예시 형식"이면 대개 충분하다. 필요한 것이 최신·사설 지식이면 RAG(6.3)가 먼저다 — 여기서 흔한 오해를 조용히 깨자. "파인튜닝으로 지식을 넣는다"는 생각은 대체로 틀렸다. 파인튜닝은 행동·형식·문체·도메인 어휘 적응에는 강하지만, 특정 사실을 암기시키는 데는 비효율적이고 최신성·출처 추적이 약하며 오히려 환각을 늘릴 수 있다. 지식은 검색으로 주입하고, 파인튜닝은 "그 지식을 어떤 톤과 형식으로 다루는가"를 각인하는 데 쓴다. 그리고 행동·형식·톤·구조화 출력·도메인 문체를 안정적으로 원할 때 파인튜닝이 답이며, 이때 1순위는 전체 FT가 아니라 PEFT다.

"LoRA는 전체 FT와 같다"는 주장의 실제 결은 이렇다. 많은 SFT·정렬·도메인 적응 태스크에서 격차가 작다는 것이 반복 관찰이고, 그래서 PEFT가 기본값이 됐다. 그러나 격차가 벌어지는 조건이 분명히 있다 — 베이스의 능력을 크게 재편해야 하는 적응(새 언어, 완전히 다른 도메인, 대규모 추가 지식), 랭크가 너무 낮아 표현 용량이 부족할 때, 매우 큰 학습 데이터로 오래 학습할 때(이때 전체 FT가 더 멀리 간다)다. 랭크는 표현 예산이라는 점을 기억하면 조정 원칙이 선다 — 랭크를 높이면 용량과 함께 과적합 위험·파라미터·메모리가 커지므로, 태스크가 단순하면 낮게, 복잡하면 높게 두고 검증셋으로 맞춘다. "무조건 큰 랭크"도 "무조건 작은 랭크"도 답이 아니다.

축프롬프트/few-shotRAGPEFT(LoRA/QLoRA)전체 FT
학습 비용00(인덱싱만)낮음높음
지식 최신화즉시(문맥)즉시(재색인)재학습 필요재학습 필요
행동·형식·문체 각인약함약함강함강함
다태스크 서빙프롬프트 스위칭인덱스 스위칭어댑터 스위칭(경제적)모델마다 GPU
품질 상한낮음~중중~높음(지식)높음(행동)최고(잠재)

큰 그림은 프롬프트 → RAG → PEFT → 전체 FT 순으로 비용과 구속이 커진다는 것이다. 가장 저렴한 단계에서 요구를 만족시키고, 부족할 때만 다음으로 넘어가라. 대부분의 팀에게 "행동을 각인해야 하는 순간"의 정답은 전체 FT가 아니라 PEFT다. 이 "make vs buy vs adapt"의 전략적 큰 그림은 유닛 이코노믹스와 함께 9.1에서 다룬다.

3.8.14 실무 — config와 하이퍼파라미터

원리를 코드로 옮겨 보자. 아래는 개념 예시이고, 라이브러리·버전에 따라 인자명·기본값이 달라진다. Hugging Face peft+transformers+bitsandbytes 스택을 가정한다.

from transformers import AutoModelForCausalLM, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model

# ① 베이스를 4비트 NF4 + 이중 양자화로 로드 (5.2)
bnb = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_quant_type="nf4",          # NormalFloat4
    bnb_4bit_use_double_quant=True,     # 이중 양자화
    bnb_4bit_compute_dtype="bfloat16",  # 계산은 bf16으로 역양자화
)
base = AutoModelForCausalLM.from_pretrained("<base-model>", quantization_config=bnb)

# ② LoRA 어댑터 부착 — 모든 선형 층 대상, 낮은 랭크
lora = LoraConfig(
    r=16,                 # 랭크 (표현 예산)
    lora_alpha=32,        # α (여기선 α/r = 2)
    lora_dropout=0.05,
    bias="none",
    task_type="CAUSAL_LM",
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
                    "gate_proj", "up_proj", "down_proj"],  # 어텐션+FFN
)
model = get_peft_model(base, lora)
model.print_trainable_parameters()   # trainable ≈ 0.1~1% (개략)
# 이후 일반 SFT 학습 루프 (3.2) — 옵티마이저는 LoRA 파라미터만 갱신

하이퍼파라미터의 출발점은 아래와 같다. 전부 태스크 의존이라 검증셋으로 조정할 대상이지 고정값이 아니다.

하이퍼파라미터흔한 출발점메모
rank $r$8~64단순 태스크 낮게, 복잡·대용량 데이터 높게
$\alpha$$r$ 또는 $2r$$\alpha/r$이 유효 스케일. $\alpha$만 키우면 스케일업
대상층모든 선형(어텐션+FFN)자원 극빈 시 $q,v$만
dropout0~0.1소규모 데이터 과적합 방지
학습률전체 FT보다 크게(예: 1e-4~3e-4)LoRA는 대개 더 큰 LR을 허용
정밀도QLoRA는 NF4 베이스+bf16 계산속도 필요 시 bf16 LoRA

학습이 끝나면 서빙 방식을 고른다.

model.save_pretrained("adapter/")        # 어댑터만 저장 (수십 MB)
# 서빙 시 선택:
merged = model.merge_and_unload()        # W' = W0 + (α/r)BA 로 병합 → 단일 모델
# 또는 병합하지 않고 vLLM 등에 베이스+어댑터로 동적 로딩 (멀티-LoRA)

인자명·기본값·권장 범위는 집필 시점 기준의 개략이며 라이브러리 API는 자주 바뀐다. 특히 target_modules의 층 이름은 모델 아키텍처마다 다르므로 반드시 해당 모델의 실제 이름을 확인해야 한다. 나머지는 이 장에서 세운 원리 그대로다 — 저랭크로 학습 파라미터를 1% 아래로 줄이고, 필요하면 베이스를 4비트로 압축해 단일 GPU에 앉히고, 병합하거나 여러 어댑터를 한 베이스에 적재해 서빙한다.

여기까지가 "적은 자원으로 한 모델을 태스크에 적응시키는" 이야기였다. 다음 장은 방향을 튼다 — 큰 모델의 능력을 작은 모델로 옮기고(지식 증류), 모델이 만든 데이터로 다시 모델을 학습시키는 법, 그리고 그때 품질이 어떻게 붕괴하는지(model collapse)를 3.9에서 다룬다. PEFT가 한 모델을 값싸게 적응시켰다면, 증류와 합성데이터는 능력과 데이터 자체를 모델로 만들어 옮기는 문제다.