LLM 엔지니어링5장 · LoRA 파인튜닝과 DPO 정렬9 / 23← 이전목차다음 →MetaContents

5장. LoRA 파인튜닝과 DPO 정렬

메모리 아끼는 기술

작은 차에 이삿짐을 싣는다고 해 봅시다. 상자를 납작하게 접고, 자주 안 쓰는 물건은 나중에 다시 사기로 하고 버리고, 한 번에 다 못 실으면 여러 번 나눠 나릅니다. 4GB GPU 에서 5억 파라미터 모델을 학습하려면 이 세 가지를 모두 씁니다.

이 PC 에서 잰 LoRA 학습의 최대 GPU 메모리를 설정에 따라 견준 막대그래프

세 막대는 이 PC(GTX 1650)에서 Qwen2.5-0.5B 에 LoRA 를 붙여 배치 8로 학습할 때 잰 최대 메모리입니다. 마젠타 막대처럼 float32 로 불러오면 5,231MB 로 청록 점선(GPU 4GB)을 넘어, 오류는 나지 않았지만 시스템 메모리를 빌려 쓰느라 아주 느렸습니다. 다홍 막대처럼 fp16 으로 불러오면 2,910MB 로 들어가고, 청록 막대처럼 그래디언트 체크포인팅까지 켜면 2,385MB 로 더 줄지만 이 PC 에서는 에포크 하나가 1.5배쯤 오래 걸렸습니다. 이 장의 실습은 가운데 설정(fp16, 체크포인팅 없음)을 씁니다. 여기서 납작하게 접은 상자가 fp16, 버렸다가 다시 사기가 체크포인팅입니다.

세 가지 기술

기술하는 일이삿짐으로 치면값
fp16 으로 불러오기기본 가중치를 16비트로 둔다상자를 납작하게 접기메모리 절반, 아주 작은 정밀도 손해
그래디언트 체크포인팅순전파의 중간값을 버렸다가 역전파 때 다시 계산버렸다가 다시 사기메모리를 크게 줄이는 대신 30% 안팎 느려진다
배치 줄이기한 번에 보는 건수를 줄인다여러 번 나눠 나르기메모리가 줄고 학습이 조금 흔들린다
혼합 정밀도(fp16=True)계산을 16비트로 하고 학습하는 값은 32비트무거운 짐만 튼튼한 상자에빠르고 메모리도 준다

GTX 1650 같은 Turing 세대 GPU 는 bf16(다른 16비트 형식)을 지원하지 않아 fp16 을 씁니다. 요즘 GPU 에서는 숫자가 덜 넘치는 bf16 을 더 많이 씁니다.

코드로 켜기

Python
from transformers import TrainingArguments

args = TrainingArguments(
    "out_demo_mem",
    per_device_train_batch_size=4,             # 배치 줄이기
    fp16=False,                                # 혼합 정밀도(GPU 가 있을 때 True)
    gradient_checkpointing=True,               # 그래디언트 체크포인팅
    gradient_checkpointing_kwargs={"use_reentrant": False},   # 최근 판의 권장 방식
    report_to="none",
)
print(args.per_device_train_batch_size, args.gradient_checkpointing)

설정만 만든 것이라 학습은 하지 않습니다. 이 예는 GPU 가 없는 곳에서도 돌도록 fp16=False 로 두었고, 실습에서는 fp16=torch.cuda.is_available() 로 GPU 가 있을 때만 켭니다.

코드에 나오는 함수

코드쉬운 뜻
TrainingArguments("out_demo_mem", ...)4장의 Trainer 에 줄 학습 설정을 만든다. 첫 값은 결과를 저장할 폴더 이름이다
per_device_train_batch_size=4GPU(장치) 하나가 한 번에 보는 학습 건수다. 줄이면 메모리가 준다
fp16=False혼합 정밀도를 끈다. GPU 가 없으면 켤 수 없어서 이 예에서는 꺼 두었다
gradient_checkpointing=True그래디언트 체크포인팅을 켠다. 중간값을 버렸다가 역전파 때 다시 계산한다
gradient_checkpointing_kwargs={"use_reentrant": False}kwargs 는 keyword arguments(이름 붙은 인자들)의 줄임말이다. 체크포인팅에 넘길 세부 설정으로, 최근 PyTorch 가 권하는 방식을 고른다
report_to="none"학습 기록을 바깥 서비스로 보내지 않는다

이 코드가 뜻하는 것

이삿짐센터에 "작은 상자로, 짐은 네 개씩, 자주 안 쓰는 것은 버렸다가 다시 사기"라고 적은 작업 지시서를 건네는 일과 같습니다. 지시서만 만들었지 아직 짐을 옮기지는 않았습니다. 여기서 작업 지시서가 TrainingArguments 입니다.

줄하는 일쉽게 말하면
args = TrainingArguments(...)메모리 아끼는 설정을 담은 학습 설정을 만든다작업 지시서를 쓴다
print(args.per_device_train_batch_size, args.gradient_checkpointing)지시서에 적힌 값을 다시 읽는다지시서를 확인한다

결과 4 True 는 배치 4와 체크포인팅이 설정에 담겼다는 확인입니다.

작게 줄여서 눈으로 보기

"상자를 납작하게 접기", 곧 fp16 이 무엇을 얻고 무엇을 잃는지 숫자 세 개로 봅니다.

Python
import torch

x32 = torch.tensor([0.1, 1000.0, 70000.0])          # float32
x16 = x32.to(torch.float16)                         # fp16 으로 바꾸기

print("숫자 하나의 바이트:", x32.element_size(), "→", x16.element_size())
print("float32:", x32.tolist())
print("fp16   :", x16.tolist())
print("fp16 이 담는 가장 큰 수:", torch.finfo(torch.float16).max)
출력
숫자 하나의 바이트: 4 → 2
float32: [0.10000000149011612, 1000.0, 70000.0]
fp16   : [0.0999755859375, 1000.0, inf]
fp16 이 담는 가장 큰 수: 65504.0
결과읽는 법
4 → 2숫자 하나가 4바이트에서 2바이트로 줄었다. 모델 전체의 메모리가 절반이 되는 까닭이다
0.1 의 줄float32 도 0.1 을 정확히 담지 못해 0.10000000149… 로 적힌다. fp16 은 0.0999755… 로 오차가 더 크다. 이것이 "아주 작은 정밀도 손해"다
1000.0이 정도 크기의 정수는 두 형식 모두 그대로 담는다
70000.0 → inffp16 이 담을 수 있는 가장 큰 수 65504 를 넘어 inf(infinity, 무한대)가 되었다. 학습 중 숫자가 이렇게 넘치면 손실이 nan 이 되기 쉽다

.to(torch.float16) 은 텐서를 fp16 으로 바꾸고, element_size() 는 숫자 하나의 바이트 수를, torch.finfo 는 floating point information(부동소수점 형식의 정보)으로 그 형식이 담는 가장 큰 수 같은 값을 알려 줍니다. 그래서 기본 가중치는 fp16 으로 두되, 학습하는 A, B 는 float32 로 두어 넘침을 피합니다.

주의할 점

  • GPU 메모리가 모자라면 Windows 는 시스템 메모리를 빌려 쓰는데(공유 메모리), 이때 학습이 아주 느려집니다. 오류 없이 느리기만 하니 nvidia-smi 로 메모리를 꼭 봅니다.
  • 메모리를 아끼는 기술은 대개 시간을 대가로 냅니다. 메모리가 넉넉하면 체크포인팅을 끄는 편이 빠릅니다.
← 이전 절5장 · 어댑터 저장과 합치기다음 절 →5장 · 선호 데이터