이미지 로딩 중...

실전 파인튜닝 완벽 가이드 Llama 3로 시작하는 나만의 AI 모델 - 슬라이드 1/7
A

AI Generated

2025. 11. 20. · 2 Views

실전 파인튜닝 완벽 가이드 Llama 3로 시작하는 나만의 AI 모델

Llama 3 8B 모델을 활용하여 실제로 파인튜닝을 진행하는 과정을 단계별로 안내합니다. 모델 로드부터 학습, 최적화, 오류 해결까지 실무에서 바로 활용할 수 있는 모든 과정을 다룹니다.


목차

  1. Llama 3 8B 모델 로드 및 준비
  2. TrainingArguments 완벽 설정
  3. SFTTrainer로 학습 시작
  4. GPU 메모리 최적화 Gradient Checkpointing
  5. 학습 중 발생하는 오류 해결
  6. 첫 번째 체크포인트 생성 및 테스트

1. Llama 3 8B 모델 로드 및 준비

시작하며

여러분이 AI 모델을 처음 학습시키려고 할 때 이런 생각을 해본 적 있나요? "도대체 이 거대한 모델을 어떻게 내 컴퓨터로 불러오지?" 막상 시작하려니 모델 파일은 수십 기가바이트고, 메모리는 부족하고, 어디서부터 손을 대야 할지 막막하기만 합니다.

이런 문제는 많은 개발자들이 AI 모델 파인튜닝에 도전할 때 가장 먼저 만나는 벽입니다. 특히 Llama 3 같은 대형 언어 모델은 크기만 수십 기가바이트에 달하기 때문에, 제대로 된 준비 없이는 로딩조차 실패하거나 컴퓨터가 멈춰버리는 경우가 많습니다.

바로 이럴 때 필요한 것이 올바른 모델 로드 방법입니다. Transformers 라이브러리를 사용하면 복잡한 과정을 단순화하고, 메모리를 효율적으로 관리하면서 안전하게 모델을 불러올 수 있습니다.

개요

간단히 말해서, 모델 로드는 AI 모델의 가중치와 구조를 메모리에 올려서 사용할 수 있는 상태로 만드는 과정입니다. 실무에서 이 단계는 매우 중요합니다.

잘못된 방법으로 로드하면 메모리 부족 오류가 발생하거나, 모델이 제대로 작동하지 않을 수 있기 때문입니다. 예를 들어, GPU 메모리가 16GB인데 8B 파라미터 모델을 그냥 로드하면 메모리가 부족해서 실패하는 경우가 많습니다.

기존에는 모델을 로드할 때 모든 가중치를 한 번에 메모리에 올렸다면, 이제는 4bit 양자화나 device_map="auto" 같은 기법을 사용해서 메모리를 절약하면서도 효율적으로 로드할 수 있습니다. 이 과정의 핵심 특징은 세 가지입니다.

첫째, BitsAndBytes를 통한 양자화로 메모리 사용량을 75% 이상 줄일 수 있습니다. 둘째, device_map을 사용하면 모델을 여러 GPU나 CPU에 자동으로 분산할 수 있습니다.

셋째, trust_remote_code 옵션으로 최신 모델의 커스텀 코드도 안전하게 실행할 수 있습니다. 이러한 특징들이 있어야 실무에서 대형 모델을 안정적으로 다룰 수 있습니다.

코드 예제

from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
import torch

# 4bit 양자화 설정 - 메모리를 75% 절약합니다
quantization_config = BitsAndBytesConfig(
    load_in_4bit=True,  # 4bit로 모델을 로드
    bnb_4bit_compute_dtype=torch.float16,  # 계산은 float16으로
    bnb_4bit_quant_type="nf4",  # 정규분포 기반 양자화
    bnb_4bit_use_double_quant=True  # 이중 양자화로 더욱 압축
)

# Llama 3 8B 모델 로드
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    quantization_config=quantization_config,
    device_map="auto",  # GPU/CPU 자동 배치
    trust_remote_code=True  # 커스텀 코드 허용
)

# 토크나이저 로드 - 텍스트를 숫자로 변환
tokenizer = AutoTokenizer.from_pretrained("meta-llama/Meta-Llama-3-8B")
tokenizer.pad_token = tokenizer.eos_token  # 패딩 토큰 설정

설명

이것이 하는 일: 이 코드는 메타의 Llama 3 8B 모델을 메모리 효율적으로 로드하고, 파인튜닝을 위한 준비를 완료합니다. 첫 번째로, BitsAndBytesConfig를 설정하는 부분이 가장 중요합니다.

load_in_4bit=True 옵션은 모델의 가중치를 32bit가 아닌 4bit로 압축해서 로드합니다. 원래 16GB가 필요한 모델을 4GB 정도로 줄일 수 있는 이유가 바로 여기에 있습니다.

bnb_4bit_compute_dtype을 float16으로 설정하면 압축된 상태에서도 계산 정확도를 유지할 수 있습니다. 그 다음으로, AutoModelForCausalLM.from_pretrained()가 실행되면서 실제 모델이 메모리에 올라갑니다.

device_map="auto" 옵션이 핵심인데, 이것은 여러분의 하드웨어 상황을 자동으로 분석해서 모델의 각 레이어를 최적의 장치(GPU0, GPU1, CPU 등)에 배치합니다. 예를 들어, GPU 메모리가 부족하면 일부 레이어는 자동으로 CPU에 올라갑니다.

세 번째로, 토크나이저를 로드하는 부분입니다. 토크나이저는 여러분이 입력하는 텍스트를 모델이 이해할 수 있는 숫자로 변환하는 도구입니다.

pad_token을 eos_token으로 설정하는 이유는 Llama 모델이 기본적으로 패딩 토큰을 가지고 있지 않기 때문입니다. 이렇게 하지 않으면 배치 학습 시 오류가 발생합니다.

마지막으로, trust_remote_code=True 옵션은 Hugging Face Hub에서 다운로드한 모델의 커스텀 코드를 실행할 수 있게 합니다. Llama 3 같은 최신 모델은 특별한 구조나 기능을 위해 커스텀 코드를 포함하고 있는데, 이 옵션 없이는 제대로 로드되지 않습니다.

여러분이 이 코드를 사용하면 고성능 GPU 없이도 8B 파라미터 모델을 로드할 수 있고, 메모리 부족 오류 없이 안정적으로 파인튜닝을 시작할 수 있습니다. 특히 양자화를 통해 모델 크기를 줄이면서도 성능 저하는 5% 미만으로 유지할 수 있다는 점이 실무에서 큰 장점입니다.

실전 팁

💡 GPU 메모리가 부족하면 load_in_8bit=True를 대신 사용해보세요. 4bit보다 메모리를 더 사용하지만 정확도는 더 높습니다.

💡 토크나이저의 padding_side를 "right"로 설정하면 인과적 언어 모델링에서 더 정확한 결과를 얻을 수 있습니다.

💡 모델 로드 전에 torch.cuda.empty_cache()를 실행하면 이전에 사용하던 GPU 메모리를 정리해서 오류를 방지할 수 있습니다.

💡 device_map="auto" 대신 device_map={"": 0}을 사용하면 모든 레이어를 GPU 0번에 강제로 배치할 수 있습니다.

💡 trust_remote_code=False로 설정하면 보안이 강화되지만, 일부 최신 모델은 로드되지 않을 수 있으니 공식 모델만 사용할 때 적용하세요.


2. TrainingArguments 완벽 설정

시작하며

여러분이 모델 학습을 시작하려고 할 때 이런 혼란을 겪어본 적 있나요? "학습률은 얼마로?

배치 사이즈는? 에포크는 몇 번?" 수십 개의 하이퍼파라미터 앞에서 어떤 값을 설정해야 할지 막막하고, 잘못 설정하면 학습이 제대로 되지 않거나 시간만 낭비하게 됩니다.

이런 문제는 딥러닝 초보자뿐만 아니라 경험 있는 개발자들도 자주 겪는 어려움입니다. 특히 대형 언어 모델의 파인튜닝에서는 하이퍼파라미터 설정이 성공과 실패를 가르는 핵심 요소입니다.

잘못된 학습률로 인해 모델이 발산하거나, 배치 사이즈가 너무 커서 메모리 오류가 발생하는 경우가 흔합니다. 바로 이럴 때 필요한 것이 TrainingArguments의 체계적인 설정입니다.

Transformers 라이브러리의 TrainingArguments를 제대로 이해하고 설정하면 학습 과정을 완벽하게 제어하고, 최적의 결과를 얻을 수 있습니다.

개요

간단히 말해서, TrainingArguments는 모델 학습의 모든 설정값을 담는 설정 객체입니다. 학습률부터 저장 주기까지 학습에 필요한 모든 것을 여기서 정의합니다.

실무에서 이 설정은 학습의 성패를 결정합니다. 올바른 하이퍼파라미터 없이는 아무리 좋은 데이터와 모델이 있어도 제대로 학습되지 않기 때문입니다.

예를 들어, GPU 메모리가 제한적인 환경에서 per_device_train_batch_size를 너무 크게 설정하면 학습이 시작조차 되지 않습니다. 기존에는 학습 스크립트에 하이퍼파라미터를 하드코딩했다면, 이제는 TrainingArguments 객체로 모든 설정을 중앙화하여 관리하고, 재사용하며, 쉽게 실험할 수 있습니다.

이 객체의 핵심 특징은 네 가지입니다. 첫째, gradient accumulation을 통해 작은 GPU에서도 큰 배치 효과를 낼 수 있습니다.

둘째, learning rate scheduler를 자동으로 설정해서 학습 안정성을 높입니다. 셋째, 체크포인트 자동 저장으로 학습 중단 시에도 복구가 가능합니다.

넷째, logging과 evaluation 주기를 세밀하게 조절하여 학습 과정을 모니터링할 수 있습니다. 이러한 특징들이 프로덕션 수준의 모델 학습을 가능하게 만듭니다.

코드 예제

from transformers import TrainingArguments

# 파인튜닝을 위한 학습 설정
training_args = TrainingArguments(
    output_dir="./llama3-finetuned",  # 모델 저장 경로
    num_train_epochs=3,  # 전체 데이터를 3번 반복 학습
    per_device_train_batch_size=4,  # 각 GPU당 배치 크기
    gradient_accumulation_steps=4,  # 4스텝마다 가중치 업데이트 (실질적 배치=16)
    learning_rate=2e-4,  # 학습률 - 파인튜닝은 작게 설정
    lr_scheduler_type="cosine",  # 코사인 스케줄러로 학습률 조정
    warmup_ratio=0.03,  # 처음 3%는 학습률을 서서히 증가
    logging_steps=10,  # 10스텝마다 로그 출력
    save_strategy="steps",  # 스텝 기준으로 저장
    save_steps=100,  # 100스텝마다 체크포인트 저장
    evaluation_strategy="steps",  # 스텝 기준으로 평가
    eval_steps=100,  # 100스텝마다 검증 데이터로 평가
    fp16=True,  # 16bit 혼합 정밀도 학습 (GPU 가속)
    optim="paged_adamw_8bit"  # 메모리 효율적인 옵티마이저
)

설명

이것이 하는 일: 이 코드는 Llama 3 모델을 효율적으로 파인튜닝하기 위한 모든 학습 매개변수를 설정합니다. 첫 번째로, output_dir과 기본 학습 파라미터를 설정합니다.

num_train_epochs=3은 전체 학습 데이터셋을 3번 반복해서 학습한다는 의미입니다. 파인튜닝에서는 보통 1-5 에포크면 충분하며, 너무 많이 하면 오버피팅이 발생합니다.

per_device_train_batch_size=4는 각 GPU에서 한 번에 4개의 샘플을 처리한다는 뜻입니다. 두 번째로, gradient_accumulation_steps=4가 매우 중요합니다.

이것은 실제로 4번의 forward/backward pass를 수행한 후에 한 번만 가중치를 업데이트한다는 의미입니다. 결과적으로 배치 크기 4 × 4 = 16의 효과를 내면서도 GPU 메모리는 배치 크기 4만큼만 사용합니다.

GPU 메모리가 부족한 환경에서 필수적인 기법입니다. 세 번째로, 학습률 관련 설정입니다.

learning_rate=2e-4는 파인튜닝에 적합한 값으로, 사전학습보다 작게 설정합니다. 너무 크면 기존 지식을 잃어버리고, 너무 작으면 학습이 느립니다.

lr_scheduler_type="cosine"은 학습률을 코사인 함수 형태로 점진적으로 감소시켜 학습 후반부의 안정성을 높입니다. warmup_ratio=0.03은 처음 3%의 스텝 동안 학습률을 0에서 시작해서 목표값까지 천천히 올리는데, 이렇게 하면 초반의 불안정한 그래디언트로 인한 문제를 방지할 수 있습니다.

네 번째로, 로깅과 저장 전략입니다. save_steps=100과 eval_steps=100을 같은 값으로 설정하면 체크포인트를 저장할 때마다 검증도 함께 수행합니다.

이렇게 하면 어느 체크포인트가 가장 성능이 좋은지 쉽게 파악할 수 있습니다. logging_steps=10은 자주 로그를 출력해서 학습 진행 상황을 실시간으로 모니터링할 수 있게 합니다.

마지막으로, fp16=True와 optim="paged_adamw_8bit"는 성능 최적화 옵션입니다. fp16은 32bit 대신 16bit 부동소수점을 사용해서 메모리를 절반으로 줄이고 계산 속도는 2-3배 빠르게 만듭니다.

paged_adamw_8bit 옵티마이저는 기존 AdamW보다 메모리를 75% 적게 사용하면서도 거의 동일한 성능을 냅니다. 여러분이 이 설정을 사용하면 제한된 GPU 리소스로도 대형 모델을 안정적으로 파인튜닝할 수 있고, 학습 과정을 완벽하게 제어하며, 최적의 체크포인트를 자동으로 저장할 수 있습니다.

특히 gradient accumulation과 8bit optimizer의 조합은 16GB GPU에서도 8B 모델을 학습 가능하게 만듭니다.

실전 팁

💡 메모리 부족 오류가 발생하면 per_device_train_batch_size를 2 또는 1로 줄이고, gradient_accumulation_steps를 그만큼 늘려서 실질적 배치 크기를 유지하세요.

💡 save_total_limit=3을 추가하면 최근 3개의 체크포인트만 유지해서 디스크 공간을 절약할 수 있습니다.

💡 report_to="wandb"를 추가하면 Weights & Biases에 학습 과정을 자동으로 기록하여 시각화하고 실험을 비교할 수 있습니다.

💡 max_grad_norm=0.3을 설정하면 그래디언트 클리핑으로 학습 안정성을 더욱 높일 수 있습니다.

💡 evaluation_strategy="no"로 설정하면 검증을 건너뛰어 학습 속도를 높일 수 있지만, 오버피팅을 감지하기 어려우니 실험 단계에서만 사용하세요.


3. SFTTrainer로 학습 시작

시작하며

여러분이 모델과 설정을 모두 준비했지만 막상 학습을 시작하려니 이런 고민에 빠진 적 있나요? "데이터를 어떻게 넣어주지?

학습 루프는 어떻게 구현하지? Loss 계산은?" 일반적인 PyTorch로 직접 학습 루프를 짜려면 수백 줄의 코드와 복잡한 로직이 필요합니다.

이런 복잡함은 많은 개발자들이 파인튜닝을 포기하게 만드는 주요 원인입니다. 데이터 로딩, 배치 처리, 그래디언트 계산, 옵티마이저 스텝, 로깅, 체크포인트 저장 등 신경 써야 할 부분이 너무 많아서 실수하기 쉽고, 디버깅도 어렵습니다.

바로 이럴 때 필요한 것이 SFTTrainer입니다. Supervised Fine-Tuning을 위해 특별히 설계된 이 트레이너는 복잡한 학습 루프를 단 몇 줄의 코드로 처리하고, 여러분은 데이터와 모델에만 집중할 수 있게 해줍니다.

개요

간단히 말해서, SFTTrainer는 언어 모델의 지도 학습 파인튜닝을 자동화하는 고수준 학습 인터페이스입니다. 실무에서 이 도구는 개발 속도를 10배 이상 향상시킵니다.

직접 학습 루프를 구현하면 며칠이 걸리는 작업을 단 몇 분 만에 시작할 수 있기 때문입니다. 예를 들어, 대화형 AI를 만들기 위해 질문-답변 데이터셋으로 Llama를 파인튜닝할 때, SFTTrainer를 사용하면 데이터만 넣어주면 나머지는 자동으로 처리됩니다.

기존에는 PyTorch의 low-level API로 epoch 루프, 배치 iteration, loss 계산, backward pass를 모두 직접 작성했다면, 이제는 SFTTrainer에 모델, 데이터, 설정만 전달하면 train() 메서드 하나로 모든 것이 해결됩니다. 이 트레이너의 핵심 특징은 다섯 가지입니다.

첫째, 자동 토큰화로 텍스트 데이터를 모델 입력으로 자동 변환합니다. 둘째, packing 기법으로 여러 샘플을 하나의 시퀀스로 결합해서 효율성을 극대화합니다.

셋째, loss masking으로 프롬프트 부분은 학습하지 않고 응답 부분만 학습합니다. 넷째, PEFT/LoRA와 자동 통합되어 메모리 효율적인 학습이 가능합니다.

다섯째, 자동 체크포인트 관리와 학습 재개 기능이 내장되어 있습니다. 이러한 특징들이 프로덕션급 파인튜닝을 누구나 쉽게 할 수 있게 만듭니다.

코드 예제

from trl import SFTTrainer
from datasets import load_dataset

# 학습 데이터 로드 (예: 대화형 데이터셋)
dataset = load_dataset("timdettmers/openassistant-guanaco", split="train")

# SFTTrainer 초기화 - 학습의 모든 것이 여기에
trainer = SFTTrainer(
    model=model,  # 이전에 로드한 Llama 3 모델
    args=training_args,  # TrainingArguments 설정
    train_dataset=dataset,  # 학습 데이터
    tokenizer=tokenizer,  # 토크나이저
    dataset_text_field="text",  # 데이터셋에서 텍스트가 있는 컬럼명
    max_seq_length=512,  # 최대 시퀀스 길이
    packing=True,  # 여러 샘플을 하나로 묶어 효율성 향상
)

# 학습 시작 - 단 한 줄로!
trainer.train()

# 학습된 모델 저장
trainer.save_model("./llama3-finetuned-final")

설명

이것이 하는 일: 이 코드는 준비된 데이터셋으로 Llama 3 모델을 자동으로 파인튜닝하고, 최적화된 모델을 저장합니다. 첫 번째로, load_dataset으로 학습 데이터를 불러옵니다.

여기서는 OpenAssistant의 Guanaco 데이터셋을 사용하는데, 이것은 고품질 대화 데이터입니다. 여러분의 데이터가 CSV, JSON, Parquet 형식이어도 load_dataset이 자동으로 처리해줍니다.

중요한 것은 데이터에 학습할 텍스트가 포함된 컬럼이 있어야 한다는 점입니다. 두 번째로, SFTTrainer를 초기화합니다.

여기서 마법이 일어나는데, model과 tokenizer를 전달하면 트레이너가 자동으로 데이터를 토큰화하고, 배치를 만들고, attention mask를 생성하고, 모델에 입력합니다. dataset_text_field="text"는 데이터셋의 "text" 컬럼을 학습에 사용하라는 의미입니다.

만약 여러분의 데이터가 "instruction"이나 "response" 같은 다른 컬럼명을 사용한다면 그에 맞게 변경하면 됩니다. 세 번째로, max_seq_length=512는 매우 중요한 설정입니다.

이보다 긴 텍스트는 잘리고, 짧은 텍스트는 패딩됩니다. 512는 메모리와 성능의 균형점인데, 더 긴 컨텍스트가 필요하면 1024나 2048로 늘릴 수 있지만 메모리 사용량이 제곱으로 증가합니다.

예를 들어, 512에서 1024로 늘리면 메모리는 4배 증가합니다. 네 번째로, packing=True는 학습 효율을 2-3배 높이는 핵심 기법입니다.

일반적으로 짧은 텍스트들을 각각 512 토큰으로 패딩하면 낭비가 심한데, packing은 여러 개의 짧은 텍스트를 하나의 512 토큰 시퀀스로 연결합니다. 예를 들어, 100토큰 텍스트 5개를 따로 처리하면 2560개의 토큰 공간이 필요하지만, packing하면 512개면 충분합니다.

이렇게 하면 GPU 활용률이 크게 증가합니다. 다섯 번째로, trainer.train()을 호출하면 실제 학습이 시작됩니다.

이 한 줄 뒤에서는 수천 줄의 코드가 실행됩니다. 데이터 로딩, 토큰화, forward pass, loss 계산, backward pass, 그래디언트 클리핑, 옵티마이저 스텝, 학습률 스케줄링, 로깅, 체크포인트 저장 등 모든 것이 자동으로 처리됩니다.

여러분은 터미널에서 학습 진행 상황을 지켜보기만 하면 됩니다. 여러분이 이 코드를 사용하면 복잡한 학습 로직 없이도 몇 분 만에 파인튜닝을 시작할 수 있고, 전문가 수준의 최적화 기법(packing, gradient checkpointing 등)을 자동으로 적용받으며, 안정적인 학습 결과를 얻을 수 있습니다.

특히 packing 기능은 학습 시간을 절반 이상 단축시켜주는 숨겨진 보석입니다.

실전 팁

💡 데이터가 instruction-response 형식이라면 formatting_func를 사용해서 "### Instruction: {instruction}\n### Response: {response}" 같은 템플릿으로 변환하세요.

💡 eval_dataset을 추가로 전달하면 학습 중 검증 데이터로 성능을 자동 평가하여 오버피팅을 조기에 발견할 수 있습니다.

💡 packing=False로 설정하면 각 샘플을 독립적으로 처리하는데, 대화 컨텍스트가 섞이면 안 되는 경우에 유용합니다.

💡 resume_from_checkpoint="./checkpoint-100"을 trainer.train()에 전달하면 중단된 학습을 정확히 그 지점부터 재개할 수 있습니다.

💡 dataset.shuffle(seed=42)로 데이터를 섞으면 학습 안정성이 향상되고, 같은 seed를 사용하면 재현 가능한 결과를 얻을 수 있습니다.


4. GPU 메모리 최적화 Gradient Checkpointing

시작하며

여러분이 학습을 시작하자마자 이런 오류를 만난 적 있나요? "CUDA out of memory" - GPU 메모리가 부족하다는 악몽 같은 메시지입니다.

배치 크기를 1로 줄여도, 모델을 양자화해도 여전히 메모리가 부족해서 학습조차 시작할 수 없는 상황이 발생합니다. 이런 문제는 대형 언어 모델 파인튜닝에서 가장 흔하게 겪는 장벽입니다.

특히 Llama 3 8B처럼 수십억 개의 파라미터를 가진 모델은 forward pass뿐만 아니라 backward pass에서도 모든 중간 활성화 값(activation)을 메모리에 저장해야 하기 때문에, 실제 모델 크기의 3-4배에 달하는 메모리가 필요합니다. 바로 이럴 때 필요한 것이 Gradient Checkpointing입니다.

이 기법은 메모리와 계산 속도를 교환하여, 메모리 사용량을 60-70% 줄이면서도 학습은 정상적으로 진행할 수 있게 해줍니다.

개요

간단히 말해서, Gradient Checkpointing은 메모리를 절약하기 위해 중간 계산 결과를 저장하지 않고 필요할 때 다시 계산하는 기법입니다. 실무에서 이 기법은 불가능했던 학습을 가능하게 만듭니다.

16GB GPU에서는 일반적으로 학습할 수 없는 모델도 Gradient Checkpointing을 켜면 학습이 가능해지기 때문입니다. 예를 들어, 배치 크기 4로 학습하려는데 메모리가 부족할 때, 이 기법을 사용하면 같은 배치 크기로 학습할 수 있습니다.

기존에는 forward pass의 모든 레이어에서 나온 활성화 값을 메모리에 저장했다가 backward pass에서 사용했다면, 이제는 일부만 저장하고 나머지는 backward 시 필요할 때 다시 계산합니다. 이 기법의 핵심 특징은 세 가지입니다.

첫째, 메모리 사용량을 60-70% 줄여서 더 큰 배치나 더 긴 시퀀스를 처리할 수 있습니다. 둘째, 학습 속도는 약 20-30% 느려지지만 메모리 절약 효과가 훨씬 큽니다.

셋째, 코드 한 줄만 추가하면 되므로 적용이 매우 쉽습니다. 이러한 특징들이 제한된 GPU 환경에서도 대형 모델 학습을 가능하게 만듭니다.

코드 예제

from transformers import AutoModelForCausalLM
import torch

# 모델 로드 (이전 코드와 동일)
model = AutoModelForCausalLM.from_pretrained(
    "meta-llama/Meta-Llama-3-8B",
    quantization_config=quantization_config,
    device_map="auto"
)

# Gradient Checkpointing 활성화 - 메모리를 60-70% 절약
model.gradient_checkpointing_enable()

# 추가 설정: input이 requires_grad를 필요로 하는 경우
model.enable_input_require_grads()

# SFTTrainer에서도 gradient checkpointing 사용
from trl import SFTTrainer

trainer = SFTTrainer(
    model=model,
    args=training_args,
    train_dataset=dataset,
    # gradient_checkpointing은 모델에서 이미 활성화됨
)

설명

이것이 하는 일: 이 코드는 Gradient Checkpointing을 활성화하여 GPU 메모리 부족 문제를 해결하고 안정적인 학습을 가능하게 합니다. 첫 번째로, model.gradient_checkpointing_enable()을 호출하면 모델의 내부 설정이 변경됩니다.

일반적인 학습에서는 32개 레이어 모두의 활성화 값을 메모리에 보관하지만, 이 기법을 켜면 예를 들어 4개 레이어마다 하나씩만 저장합니다. Llama 3의 경우 32개 레이어 중 8개만 저장하고 나머지는 버립니다.

두 번째로, backward pass가 실행될 때 진짜 마법이 일어납니다. 그래디언트를 계산하려면 모든 레이어의 활성화 값이 필요한데, 우리는 8개만 저장했습니다.

이때 저장하지 않은 레이어의 활성화 값은 저장된 체크포인트부터 다시 forward pass를 실행해서 재계산합니다. 예를 들어, 레이어 10의 그래디언트가 필요하면 레이어 8(체크포인트)부터 레이어 10까지 다시 계산합니다.

세 번째로, model.enable_input_require_grads()는 특수한 경우에 필요합니다. 일부 모델 구조에서는 입력 임베딩에 대한 그래디언트 계산이 필요한데, Gradient Checkpointing과 함께 사용할 때 이 설정을 켜지 않으면 오류가 발생할 수 있습니다.

특히 임베딩 레이어를 파인튜닝하는 경우 필수입니다. 네 번째로, 메모리 절약 효과를 구체적으로 살펴보겠습니다.

예를 들어, 일반 학습에서 16GB 메모리를 사용한다면 Gradient Checkpointing을 켜면 약 6-7GB만 사용합니다. 이 차이로 배치 크기를 2에서 4로 늘리거나, 시퀀스 길이를 512에서 1024로 늘릴 수 있습니다.

대신 학습 시간은 약 20-30% 증가하는데, 이는 재계산 때문입니다. 다섯 번째로, TrainingArguments에서 gradient_checkpointing=True를 설정하는 방법도 있지만, 모델에 직접 설정하는 것이 더 명시적이고 확실합니다.

두 방법 모두 같은 효과를 내지만, 모델에 직접 설정하면 Trainer를 사용하지 않는 커스텀 학습 루프에서도 작동합니다. 여러분이 이 기법을 사용하면 GPU 메모리 부족 오류를 해결하고, 더 큰 배치 크기로 학습 안정성을 높이며, 제한된 하드웨어로도 대형 모델 파인튜닝에 도전할 수 있습니다.

특히 8GB-16GB GPU를 사용하는 개인 개발자에게는 필수적인 기법입니다.

실전 팁

💡 메모리가 여전히 부족하면 TrainingArguments에서 gradient_accumulation_steps를 늘려서 실질적 배치 크기를 유지하면서 per_device_batch_size를 줄이세요.

💡 학습 속도 저하가 심하면 use_reentrant=False를 model.gradient_checkpointing_enable(use_reentrant=False)로 설정하면 일부 케이스에서 더 빠릅니다.

💡 추론(inference) 시에는 반드시 model.gradient_checkpointing_disable()로 비활성화하세요. 추론에는 필요 없고 속도만 느려집니다.

💡 torch.cuda.memory_summary()를 사용하면 메모리 사용 패턴을 분석하여 Gradient Checkpointing의 효과를 정확히 측정할 수 있습니다.

💡 매우 긴 시퀀스(2048+)를 사용할 때는 Flash Attention과 함께 사용하면 메모리 절약 효과가 배가됩니다.


5. 학습 중 발생하는 오류 해결

시작하며

여러분이 학습을 시작했는데 갑자기 이런 오류들을 만난 적 있나요? "RuntimeError: Expected all tensors to be on the same device", "ValueError: Attention mask and position ids are wrong", "Loss is NaN" - 학습이 중간에 멈추거나 이상한 결과가 나오는 상황은 정말 답답합니다.

이런 문제들은 파인튜닝 과정에서 누구나 겪는 보편적인 어려움입니다. 특히 대형 언어 모델은 복잡한 구조와 많은 설정 옵션 때문에 작은 실수 하나가 큰 오류로 이어지기 쉽습니다.

몇 시간씩 학습이 진행되다가 갑자기 멈추면 시간과 GPU 비용이 모두 낭비됩니다. 바로 이럴 때 필요한 것이 체계적인 오류 해결 방법입니다.

흔히 발생하는 오류들의 원인을 이해하고, 미리 예방하며, 빠르게 해결하는 방법을 알면 학습 과정이 훨씬 순조로워집니다.

개요

간단히 말해서, 파인튜닝 오류는 대부분 데이터 형식, 디바이스 불일치, 메모리 부족, 수치 안정성 문제로 분류할 수 있습니다. 실무에서 오류 해결 능력은 파인튜닝 성공의 핵심입니다.

같은 오류를 만났을 때 몇 분 만에 해결하는 개발자와 몇 시간을 헤매는 개발자의 차이는 경험과 지식에서 나오기 때문입니다. 예를 들어, "Loss is NaN" 오류는 학습률이 너무 높거나, 그래디언트가 폭발했거나, 데이터에 이상치가 있다는 신호인데 이를 모르면 원인을 찾기 어렵습니다.

기존에는 오류 메시지를 검색하고 Stack Overflow를 뒤지며 시간을 낭비했다면, 이제는 흔한 오류 패턴과 해결 방법을 미리 알고 체크리스트처럼 확인하여 빠르게 해결할 수 있습니다. 오류 해결의 핵심 전략은 네 가지입니다.

첫째, 오류 메시지의 스택 트레이스를 끝까지 읽어서 정확한 발생 지점을 파악합니다. 둘째, 작은 데이터셋으로 먼저 테스트하여 오류를 빠르게 재현합니다.

셋째, 설정을 하나씩 제거하며 최소 재현 케이스를 찾습니다. 넷째, 로깅을 추가해서 중간 값들을 확인합니다.

이러한 전략들이 복잡한 오류도 체계적으로 해결할 수 있게 만듭니다.

코드 예제

# 오류 1: CUDA Out of Memory 해결
# 해결책: 배치 크기 줄이기 + Gradient Accumulation
training_args = TrainingArguments(
    per_device_train_batch_size=1,  # 2에서 1로 줄임
    gradient_accumulation_steps=8,  # 4에서 8로 늘림
    gradient_checkpointing=True,  # 반드시 활성화
)

# 오류 2: Loss is NaN 해결
# 해결책: 학습률 낮추기 + Gradient Clipping
training_args = TrainingArguments(
    learning_rate=1e-5,  # 2e-4에서 낮춤
    max_grad_norm=1.0,  # 그래디언트 클리핑 추가
    fp16=False,  # 또는 bf16=True 사용
)

# 오류 3: 토크나이저 패딩 오류 해결
tokenizer.pad_token = tokenizer.eos_token
tokenizer.padding_side = "right"  # 명시적으로 설정

# 오류 4: 데이터 형식 검증
def validate_dataset(dataset):
    sample = dataset[0]
    print(f"Sample keys: {sample.keys()}")
    print(f"Text length: {len(sample['text'])}")
    # 너무 긴 텍스트 필터링
    dataset = dataset.filter(lambda x: len(x['text']) < 4096)
    return dataset

dataset = validate_dataset(dataset)

설명

이것이 하는 일: 이 코드는 파인튜닝 중 가장 자주 발생하는 오류들을 예방하고 해결하는 설정과 검증 로직을 제공합니다. 첫 번째로, CUDA Out of Memory 오류는 가장 흔한 문제입니다.

이 오류가 발생하면 per_device_train_batch_size를 절반으로 줄이고, gradient_accumulation_steps를 두 배로 늘려서 실질적인 배치 크기는 유지합니다. 예를 들어, 배치 4 × accumulation 4 = 16이었다면, 배치 2 × accumulation 8 = 16으로 같은 효과를 내면서 메모리는 절반만 사용합니다.

gradient_checkpointing=True는 필수이며, 이것만으로도 메모리를 60% 절약할 수 있습니다. 두 번째로, Loss is NaN 오류는 학습이 수치적으로 불안정해졌다는 신호입니다.

주요 원인은 너무 높은 학습률입니다. learning_rate를 2e-4에서 1e-5나 5e-6으로 낮추면 대부분 해결됩니다.

max_grad_norm=1.0은 그래디언트 클리핑으로, 어떤 파라미터의 그래디언트도 1.0을 넘지 못하게 제한합니다. 이렇게 하면 그래디언트 폭발을 방지할 수 있습니다.

fp16=False로 설정하거나 bf16=True로 전환하는 것도 효과적인데, bf16(bfloat16)은 fp16보다 수치 범위가 넓어 NaN이 덜 발생합니다. 세 번째로, 토크나이저 관련 오류는 주로 패딩 토큰 미설정 때문입니다.

Llama 모델은 기본적으로 pad_token이 없어서 배치 처리 시 오류가 발생합니다. tokenizer.pad_token = tokenizer.eos_token으로 설정하면 문장 끝 토큰을 패딩으로 사용합니다.

padding_side="right"는 텍스트의 오른쪽에 패딩을 추가하라는 의미인데, 인과적 언어 모델(causal LM)에서는 이것이 표준입니다. "left"로 설정하면 attention mask가 잘못 계산될 수 있습니다.

네 번째로, 데이터 검증 함수는 학습 전에 반드시 실행해야 합니다. validate_dataset은 첫 번째 샘플을 출력해서 데이터 구조가 올바른지 확인합니다.

예를 들어, dataset_text_field="text"로 설정했는데 실제로는 "content"라는 컬럼명을 사용하면 오류가 발생합니다. 또한 4096 토큰을 넘는 매우 긴 텍스트는 메모리 문제를 일으킬 수 있어서 필터링합니다.

dataset.filter()는 조건에 맞는 샘플만 남깁니다. 다섯 번째로, 오류가 발생했을 때의 디버깅 전략입니다.

먼저 데이터셋을 .select(range(10))으로 10개만 남겨서 빠르게 테스트합니다. 오류가 재현되면 설정을 하나씩 제거하며 원인을 찾습니다.

예를 들어, packing=True를 끄거나, max_seq_length를 줄이거나, fp16을 끕니다. 오류 메시지의 마지막 줄만 보지 말고 전체 스택 트레이스를 읽어서 어느 파일의 몇 번째 줄에서 발생했는지 파악합니다.

여러분이 이러한 오류 해결 방법을 익히면 학습 중 문제가 발생해도 당황하지 않고 몇 분 만에 해결할 수 있으며, 사전에 검증하여 오류를 예방하고, 안정적인 학습 파이프라인을 구축할 수 있습니다. 특히 데이터 검증은 학습 시작 전에 항상 수행하는 것이 좋습니다.

실전 팁

💡 오류 발생 시 CUDA_LAUNCH_BLOCKING=1 환경변수를 설정하면 정확한 오류 발생 지점을 찾을 수 있습니다.

💡 torch.autograd.set_detect_anomaly(True)를 학습 전에 호출하면 NaN이나 Inf가 발생하는 순간을 잡아낼 수 있지만 속도가 느려지므로 디버깅 시에만 사용하세요.

💡 logging_steps=1로 설정하고 loss를 매 스텝마다 확인하면 어느 시점에서 문제가 시작되는지 정확히 파악할 수 있습니다.

💡 resume_from_checkpoint로 학습을 재개할 때 오류가 발생하면 optimizer state 파일이 손상되었을 수 있으니 해당 체크포인트를 삭제하고 이전 체크포인트부터 다시 시작하세요.

💡 데이터에 특수 문자나 이모지가 많으면 토크나이저 오류가 발생할 수 있으니 text.encode('utf-8', errors='ignore').decode('utf-8')로 정리하세요.


6. 첫 번째 체크포인트 생성 및 테스트

시작하며

여러분이 몇 시간 동안 학습을 진행했는데 이런 궁금증이 생긴 적 있나요? "지금까지 학습된 모델이 실제로 잘 작동하는지 어떻게 확인하지?

학습이 끝날 때까지 기다려야 하나?" 학습이 끝나고 나서야 모델이 제대로 학습되지 않았다는 것을 발견하면 모든 시간이 낭비됩니다. 이런 문제는 체크포인트 관리를 제대로 하지 않아서 발생합니다.

학습 중간에 저장된 모델을 테스트하지 않으면 잘못된 방향으로 학습이 진행되어도 알 수 없고, 최적의 체크포인트를 놓칠 수도 있습니다. 예를 들어, 500스텝에서 가장 좋은 성능을 보였는데 1000스텝까지 학습하면서 오버피팅이 발생할 수 있습니다.

바로 이럴 때 필요한 것이 체크포인트 기반의 점진적 평가입니다. 학습 중간중간 저장되는 체크포인트를 로드하여 실제로 테스트해보면 학습 진행 상황을 파악하고, 최적의 지점을 찾으며, 문제를 조기에 발견할 수 있습니다.

개요

간단히 말해서, 체크포인트는 특정 학습 단계의 모델 상태를 저장한 스냅샷이며, 이를 테스트하여 학습 품질을 평가하는 것이 핵심입니다. 실무에서 체크포인트 관리는 성공적인 파인튜닝의 필수 요소입니다.

프로덕션 환경에서는 최종 모델이 아니라 검증 성능이 가장 좋은 체크포인트를 사용하기 때문입니다. 예를 들어, 고객 서비스 챗봇을 파인튜닝할 때 300스텝 체크포인트가 가장 자연스러운 응답을 생성한다면 최종 1000스텝 모델이 아니라 300스텝 모델을 배포해야 합니다.

기존에는 학습이 완전히 끝난 후에야 모델을 테스트했다면, 이제는 save_steps마다 자동 저장되는 체크포인트를 즉시 로드하여 실시간으로 품질을 확인하고 학습 방향을 조정할 수 있습니다. 체크포인트 테스트의 핵심 특징은 다섯 가지입니다.

첫째, from_pretrained로 체크포인트를 일반 모델처럼 로드할 수 있습니다. 둘째, generate() 메서드로 실제 텍스트를 생성하여 정성적 평가를 수행합니다.

셋째, 프롬프트 템플릿을 사용하여 실제 사용 시나리오와 동일하게 테스트합니다. 넷째, 여러 체크포인트를 비교하여 최적의 것을 선택합니다.

다섯째, 조기 종료(early stopping) 결정을 내릴 수 있습니다. 이러한 특징들이 데이터 기반의 합리적인 모델 선택을 가능하게 합니다.

코드 예제

from transformers import AutoModelForCausalLM, AutoTokenizer
import torch

# 체크포인트 로드 (예: 100스텝 체크포인트)
checkpoint_path = "./llama3-finetuned/checkpoint-100"
model = AutoModelForCausalLM.from_pretrained(
    checkpoint_path,
    device_map="auto",
    torch_dtype=torch.float16
)
tokenizer = AutoTokenizer.from_pretrained(checkpoint_path)

# 테스트 프롬프트 준비
prompt = "### Instruction: 파이썬에서 리스트를 정렬하는 방법을 알려주세요.\n### Response:"

# 토큰화 및 생성
inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
outputs = model.generate(
    **inputs,
    max_new_tokens=256,  # 최대 256개의 새 토큰 생성
    temperature=0.7,  # 창의성 조절 (0.1=보수적, 1.0=창의적)
    top_p=0.9,  # nucleus sampling
    do_sample=True,  # 샘플링 활성화
    pad_token_id=tokenizer.eos_token_id
)

# 결과 디코딩 및 출력
response = tokenizer.decode(outputs[0], skip_special_tokens=True)
print(response)

설명

이것이 하는 일: 이 코드는 학습 중 저장된 체크포인트를 로드하고, 실제 프롬프트로 텍스트를 생성하여 모델의 성능을 평가합니다. 첫 번째로, AutoModelForCausalLM.from_pretrained()로 체크포인트를 로드합니다.

checkpoint-100은 TrainingArguments의 save_steps=100 설정에 따라 100스텝마다 자동 저장된 폴더입니다. 이 폴더 안에는 모델 가중치(model.safetensors), 설정(config.json), 토크나이저 파일들이 모두 들어 있어서 완전한 모델로 로드할 수 있습니다.

torch_dtype=torch.float16을 사용하면 메모리를 절반으로 줄이면서도 추론 품질은 거의 동일합니다. 두 번째로, 프롬프트를 준비합니다.

"### Instruction: ... ### Response:" 형식은 많은 instruction-following 모델들이 사용하는 표준 템플릿입니다.

여러분이 학습 데이터를 이 형식으로 준비했다면 테스트할 때도 같은 형식을 사용해야 정확한 평가가 가능합니다. 프롬프트는 모델이 학습한 task를 대표하는 것이어야 하며, 너무 쉽거나 너무 어려운 것은 피해야 합니다.

세 번째로, tokenizer()로 텍스트를 토큰으로 변환하고 .to(model.device)로 모델과 같은 디바이스(GPU)로 이동시킵니다. return_tensors="pt"는 PyTorch 텐서 형식으로 반환하라는 의미입니다.

이 과정에서 텍스트는 숫자 배열로 변환되는데, 예를 들어 "파이썬"은 [12345, 67890] 같은 토큰 ID로 바뀝니다. 네 번째로, model.generate()가 핵심입니다.

max_new_tokens=256은 프롬프트 이외에 최대 256개의 새 토큰을 생성한다는 뜻입니다. temperature=0.7은 생성의 무작위성을 조절하는데, 0.1이면 항상 가장 확률이 높은 단어를 선택해서 안전하지만 반복적인 답변을 생성하고, 1.0이면 매우 창의적이지만 때로는 말이 안 되는 답변을 만듭니다.

0.7은 적절한 균형점입니다. top_p=0.9는 nucleus sampling으로, 누적 확률이 90%가 되는 단어들만 후보로 고려합니다.

이렇게 하면 너무 이상한 단어는 제외되면서도 다양성은 유지됩니다. 다섯 번째로, tokenizer.decode()로 생성된 토큰 ID를 다시 텍스트로 변환합니다.

skip_special_tokens=True는 <s>, </s> 같은 특수 토큰을 출력에서 제거합니다. outputs[0]을 사용하는 이유는 generate()가 배치 형태로 결과를 반환하기 때문에 첫 번째(유일한) 샘플을 선택하는 것입니다.

여러분이 이 방법을 사용하면 학습 중간에도 모델 품질을 확인하고, 체크포인트-200, 체크포인트-400, 체크포인트-600을 각각 테스트하여 가장 좋은 것을 찾으며, 만약 성능이 더 이상 향상되지 않으면 학습을 조기 종료하여 시간과 비용을 절약할 수 있습니다. 특히 여러 프롬프트로 테스트하면 모델의 강점과 약점을 정확히 파악할 수 있습니다.

실전 팁

💡 여러 체크포인트를 비교할 때는 동일한 프롬프트 세트를 사용하고 seed를 고정(torch.manual_seed(42))하여 공정한 비교를 하세요.

💡 temperature=0.1로 설정하면 deterministic한 결과를 얻어 재현 가능한 테스트가 가능합니다.

💡 repetition_penalty=1.2를 추가하면 모델이 같은 문장을 반복하는 것을 방지할 수 있습니다.

💡 체크포인트 폴더의 trainer_state.json을 읽으면 해당 스텝의 정확한 loss와 learning rate를 확인할 수 있습니다.

💡 프로덕션 배포 전에는 최소 10-20개의 다양한 프롬프트로 테스트하여 모델의 일반화 능력을 검증하세요.


#Python#Llama3#파인튜닝#Transformers#SFTTrainer#AI,LLM,머신러닝,파인튜닝,NLP

댓글 (0)

댓글을 작성하려면 로그인이 필요합니다.