8장. PEFT와 LoRA 학습
Full Fine-tuning 이 부담스러운 까닭
이삿짐을 옮긴다고 해 봅시다. 가구만 옮기면 트럭 한 대면 됩니다. 그런데 가구마다 "어느 방향으로 옮길지 적은 쪽지"와 "지난번에 어떻게 옮겼는지 적은 기록장 두 권"까지 함께 실어야 한다면 짐이 네 배가 됩니다. 모델을 학습할 때가 이렇습니다. 답을 만들기만 하는 추론(inference)에는 가중치만 있으면 되지만, 학습할 때는 가중치마다 그래디언트와 옵티마이저 기록이 따라붙습니다.
그림의 왼쪽 첫 칸(가중치)만 추론에 필요합니다. 학습에서는 둘째 칸(그래디언트)과 셋째 칸(옵티마이저 상태)이 더해져, 넷째 칸처럼 파라미터 하나가 16바이트를 차지합니다. 이 그림은 FP32(숫자 하나에 4바이트)로 모두 저장한다고 가정한 어림값입니다. 실제로는 입력과 답을 처리하는 중간 계산(activation)과, 어휘 151,936 개 전체에 대한 점수표(logits)를 담을 메모리가 더 필요합니다.
파라미터 하나가 차지하는 메모리
| 항목 | 추론할 때 | 학습할 때 | 하는 일 | 이삿짐으로 치면 |
|---|---|---|---|---|
| 가중치 | 4바이트 | 4바이트 | 모델이 학습한 현재 값 | 가구 |
| 그래디언트 | 없음 | 4바이트 | 가중치를 어느 방향으로 바꿀지 | 옮길 방향 쪽지 |
| 옵티마이저 상태 | 없음 | 8바이트 | 지난 그래디언트를 요약한 숫자 두 개 | 기록장 두 권 |
| 합계 | 약 4바이트 | 약 16바이트 | 파라미터와 직접 이어진 메모리만 | 짐이 네 배 |
손으로 한 번 계산해 봅니다. 메모리는 "파라미터 수 × 바이트 수"입니다.
| 모델 | 파라미터 수 | 추론(× 4바이트) | 전체 학습(× 16바이트) |
|---|---|---|---|
| Qwen2.5-0.5B | 494,032,768 | 1,976,131,072 바이트 ≈ 1,885 MB | 7,904,524,288 바이트 ≈ 7.36 GB |
| 8B 모델 | 8,000,000,000 | 32,000,000,000 바이트 ≈ 29.8 GB | 128,000,000,000 바이트 ≈ 119 GB |
여기서 MB 와 GB 는 1024 단위입니다(1 MB = 1024 × 1024 바이트). 0.5B 모델도 전체 학습에는 activation 을 빼고도 7GB 넘게 필요하므로, 10GB GPU 로는 빠듯합니다. 8B 모델은 119GB 가까이 필요해 비싼 GPU 여러 장이 있어야 합니다.
부담은 메모리만이 아닙니다. 저장 공간도 있습니다. Full Fine-tuning 은 업무마다 모델 전체를 따로 저장해야 합니다(8B 를 BF16 으로 저장하면 16GB). LoRA 는 업무마다 수십 MB 의 어댑터만 저장합니다.
짧은 예시
# 파라미터 수 × 바이트 수로 메모리를 어림한다
BYTES = {"가중치": 4, "그래디언트": 4, "옵티마이저 상태": 8} # FP32 기준, 파라미터 하나당
def memory_mb(n_params, training):
# training=True 면 세 항목을 모두, False(추론)면 가중치만 더한다
per_param = sum(BYTES.values()) if training else BYTES["가중치"]
# 1024**2 로 나누어 MB 단위로 바꾼다
return n_params * per_param / 1024**2
qwen = 494_032_768 # 6차시에서 센 Qwen2.5-0.5B 의 파라미터 수
print(f"추론 : {memory_mb(qwen, training=False):,.0f} MB") # 결과: 추론 : 1,885 MB
print(f"학습 : {memory_mb(qwen, training=True):,.0f} MB") # 결과: 학습 : 7,538 MB
print(f"8B 학습: {memory_mb(8e9, training=True) / 1024:,.1f} GB") # 결과: 8B 학습: 119.2 GB이 코드가 뜻하는 것
이삿짐 목록표를 만들어 트럭 크기를 미리 재 본 코드입니다. 가구만 싣는 경우(추론)와 쪽지와 기록장까지 싣는 경우(학습)를 견줍니다. 여기서 짐 목록이 BYTES, 가구 수가 n_params 입니다.
| 줄 | 하는 일 | 쉽게 말하면 |
|---|---|---|
BYTES = {...} | 항목별 바이트 수를 사전에 담는다 | 짐 종류별 부피표 |
sum(BYTES.values()) | 4 + 4 + 8 = 16 | 학습 때 한 개당 짐 |
n_params * per_param / 1024**2 | 바이트를 MB 로 바꾼다 | 전체 짐 부피 |
1,885 MB 는 실습 4에서 어댑터를 저장할 때 원본 코드가 찍는 "기반 모델 가중치(FP32) 크기 약 1,885 MB"와 같은 값입니다. 학습하면 16바이트로 계산해 네 배인 7,538 MB 가 됩니다(1,885 는 반올림한 값이라 4 를 곱하면 7,540 이 됩니다).
주의할 점
- 표의 16바이트는 파라미터와 직접 이어진 메모리만 센 어림값입니다. 실제 학습에는 activation, logits(
배치 × 길이 × 151,936) 같은 메모리가 더해지고, 배치 크기와 문장 길이를 늘리면 더 커집니다. - BF16(숫자 하나에 2바이트)으로 계산하면 줄어들지만, 옵티마이저 상태는 보통 FP32 로 둡니다. 정밀도 설정에 따라 실제 값은 달라집니다.
- GPU 메모리의 실제 사용량은 명령 창에서
nvidia-smi로 확인합니다.