본문 바로가기
IT/AI

[AI] LLM 파인튜닝 실전 가이드: LoRA, QLoRA로 커스텀 모델 만들기

by 수누다 2026. 5. 14.

LLM 파인튜닝 실전 가이드: LoRA, QLoRA로 커스텀 모델 만들기

안녕하세요, 13년차 인프라 엔지니어 '서버실'입니다. 요즘 LLM(Large Language Model)의 세상이 정말 빠르게 변하고 있죠? ChatGPT 같은 거대 모델들을 보면서, '아, 나도 우리 회사 데이터로, 혹은 나만의 특화된 지식으로 LLM을 만들 수 없을까?' 하는 생각, 혹시 해보셨나요?

사실 저도 이런 꿈을 꾸다가 거대한 모델을 통째로 학습시키려면 엄청난 GPU 자원이 필요하다는 현실의 벽에 부딪혔어요. 엔비디아(NVIDIA) H100 같은 최신 GPU는 가격도 만만치 않고요. 게다가 학습 시간도 정말 어마어마하더라고요. 홈랩에서 저만의 서버를 돌리는 저 같은 사람에게는 엄두도 못 낼 일이었습니다. 😅

하지만 희망은 있더라고요! 바로 PEFT(Parameter-Efficient Fine-Tuning, 파라미터 효율적 파인튜닝) 기법 덕분입니다. 특히 LoRA(Low-Rank Adaptation)와 이를 더 발전시킨 QLoRA(Quantized LoRA)는 제한된 자원으로도 LLM 파인튜닝을 가능하게 해주는 정말 혁신적인 방법이거든요.

오늘은 제가 직접 여러 모델을 파인튜닝하며 겪었던 삽질 경험과 함께, LoRA와 QLoRA를 활용해서 나만의 커스텀 LLM을 만드는 실전 가이드를 여러분께 공유해드릴게요. 자, 그럼 함께 시작해볼까요?

LLM 파인튜닝의 핵심, LoRA와 QLoRA의 기본 원리를 한눈에 볼 수 있는 다이어그램입니다.

핵심 개념 설명: PEFT, LoRA, QLoRA 이해하기

본격적인 실전에 앞서, 핵심 개념들을 짚고 넘어가는 게 중요해요. 제가 처음엔 이 용어들이 좀 헷갈렸거든요.

  • LLM (Large Language Model, 거대 언어 모델): 방대한 텍스트 데이터를 학습하여 사람의 언어를 이해하고 생성하는 능력을 가진 AI 모델입니다. 예를 들면 GPT-3, Llama 2 같은 모델들이죠.
  • Fine-tuning (파인튜닝): 이미 학습된 거대 모델(Pre-trained Model)을 특정 작업이나 데이터셋에 맞게 추가로 학습시키는 과정입니다. 예를 들어, 의료 질문 답변에 특화된 모델을 만들고 싶다면, 의료 데이터로 파인튜닝하는 식이죠.
  • PEFT (Parameter-Efficient Fine-Tuning, 파라미터 효율적 파인튜닝): 이 녀석이 바로 우리의 구세주입니다! 기존의 파인튜닝은 모델 전체의 모든 파라미터를 업데이트해야 해서 엄청난 자원이 필요했어요. 하지만 PEFT는 모델의 아주 작은 부분(소수의 파라미터)만 학습시키거나, 기존 모델에 작은 모듈을 추가해서 학습 효율을 극대화하는 기법들을 총칭합니다. 덕분에 적은 GPU 메모리로도 파인튜닝이 가능해지는 거죠.
  • LoRA (Low-Rank Adaptation, 저랭크 적응): PEFT의 대표적인 방법 중 하나에요. 쉽게 말해, 거대한 LLM의 가중치(weights)를 직접 수정하는 대신, 원본 가중치 옆에 아주 작은 두 개의 행렬(low-rank matrices)을 추가해서 학습시키는 방식입니다. 이 작은 행렬들만 학습시키고, 원본 모델의 가중치는 그대로 두는 거죠. 이렇게 하면 학습해야 할 파라미터 수가 극적으로 줄어들고, 학습된 어댑터(adapter)의 크기도 매우 작아서 저장 및 로드도 훨씬 수월해요.
  • QLoRA (Quantized LoRA, 양자화된 LoRA): LoRA를 한 단계 더 발전시킨 기법이에요. 기존 LoRA는 원본 모델의 가중치를 16비트 부동소수점(FP16)으로 불러와야 했는데, QLoRA는 이를 4비트 정수(4-bit quantization)로 양자화(quantization)해서 불러옵니다. 이렇게 되면 모델을 로드하는 데 필요한 GPU 메모리가 획기적으로 줄어들어요. 제가 홈랩에서 GPU 메모리가 부족해서 겪었던 수많은 삽질 끝에 찾은 빛과 같은 존재였습니다! 덕분에 8GB나 12GB GPU로도 7B(70억 개 파라미터) 이상의 LLM을 파인튜닝할 수 있게 된 거죠.

LLM 파인튜닝 실전 구현: LoRA, QLoRA 단계별 적용

자, 이제 이론은 충분해요! 제가 직접 홈랩에서 시도했던 과정을 바탕으로, LoRA와 QLoRA를 활용한 모델 학습 실전 가이드를 단계별로 알려드릴게요.

1. 환경 준비

먼저 필요한 라이브러리들을 설치해야 합니다. 파이썬(Python) 환경은 기본이겠죠? 저는 주로 가상 환경(Virtual Environment)을 만들어서 작업하는데, 깔끔하더라고요.

# 가상 환경 생성 및 활성화
python -m venv llm_finetune_env
source llm_finetune_env/bin/activate

# 필요한 라이브러리 설치
# transformers: Hugging Face 모델 및 트레이너
# peft: 파라미터 효율적 파인튜닝 (LoRA, QLoRA 등)
# bitsandbytes: 4비트 양자화 지원
# accelerate: 분산 학습 가속화
# datasets: 데이터셋 로드 및 처리
# trl: 트랜스포머 강화 학습 (SFTTrainer 사용)
pip install transformers peft bitsandbytes accelerate datasets trl torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121

여기서 <code>torch 설치 시 CUDA 버전에 맞는 URL을 사용해야 해요. 저는 CUDA 12.1 환경이라 cu121을 썼지만, 여러분의 환경에 맞춰 조절해주세요. ⚠️ GPU 드라이버와 CUDA Toolkit 버전이 제대로 맞지 않으면 모델 학습 시 에러가 발생할 수 있으니 꼭 확인하셔야 합니다!

2. 데이터셋 준비

파인튜닝에 사용할 데이터셋을 준비해야 합니다. 저는 간단한 질문-답변 형식의 JSON 파일을 사용했어요. 실제 프로젝트에서는 여러분의 목적에 맞는 데이터를 잘 정제하는 것이 무엇보다 중요합니다.

# example_dataset.json
[
  {
    "instruction": "다음 질문에 대해 간결하게 답변해 주세요.",
    "input": "LLM 파인튜닝이 무엇인가요?",
    "output": "LLM 파인튜닝은 이미 학습된 거대 언어 모델을 특정 작업이나 데이터셋에 맞게 추가로 학습시키는 과정입니다."
  },
  {
    "instruction": "LoRA의 장점을 설명해 주세요.",
    "input": "",
    "output": "LoRA는 원본 모델의 가중치를 고정하고 작은 행렬만 학습하여, 파라미터 수를 획기적으로 줄이고 학습 효율을 높이는 파인튜닝 기법입니다."
  }
]

이 데이터를 파이썬에서 불러와서 datasets 라이브러리로 처리하면 돼요.

3. 모델 로드 및 양자화 설정 (QLoRA)

이제 Hugging Face에서 적당한 LLM 모델을 불러옵니다. 저는 Llama 2 7B 모델을 예시로 들어볼게요. QLoRA를 사용하려면 bitsandbytes 설정을 잘 해줘야 합니다.

import torch
from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig

# 모델 ID (예시: 메타의 Llama-2 7B 모델)
model_id = "meta-llama/Llama-2-7b-hf" # 실제 사용 시 접근 권한 필요

# 4비트 양자화 설정
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True, # 4비트 양자화로 로드
    bnb_4bit_use_double_quant=True, # 이중 양자화 사용
    bnb_4bit_quant_type="nf4", # NormalFloat 4 (NF4) 양자화 타입
    bnb_4bit_compute_dtype=torch.bfloat16 # 계산 시 사용할 데이터 타입
)

# 토크나이저 및 모델 로드
tokenizer = AutoTokenizer.from_pretrained(model_id)
model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto" # 여러 GPU가 있다면 자동으로 분배
)
model.config.use_cache = False # 학습 중 캐시 비활성화
model.config.pretraining_tp = 1 # 사전 학습 텐서 병렬 처리 설정

여기서 model_id는 실제 접근 가능한 모델 ID로 변경하셔야 해요. Llama 2 같은 모델은 Hugging Face에서 접근 권한을 요청해야 하거든요. 저는 보통 공개된 모델 중 하나를 선택해서 실험합니다.

4. LoRA 설정 및 모델 학습 준비

peft 라이브러리를 사용해서 LoRAConfig를 정의합니다. 여기서 r (LoRA 랭크)lora_alpha 값이 정말 중요해요.

from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

# 4비트 학습을 위해 모델 준비
model = prepare_model_for_kbit_training(model)

# LoRA 설정
lora_config = LoraConfig(
    r=16, # LoRA 랭크. 값이 클수록 표현력이 좋지만 파라미터도 늘어남.
    lora_alpha=32, # LoRA 스케일링 계수. r의 두 배 정도가 일반적.
    target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], # LoRA를 적용할 모듈
    bias="none", # 편향(bias) 학습 여부. 'none'이 일반적.
    lora_dropout=0.05, # LoRA 레이어에 적용할 드롭아웃
    task_type="CAUSAL_LM", # 작업 유형 (인과 언어 모델)
)

# PEFT 모델 생성
model = get_peft_model(model, lora_config)
model.print_trainable_parameters() # 학습 가능한 파라미터 수 확인

target_modules는 LoRA를 적용할 트랜스포머 블록 내의 레이어들을 지정해요. 주로 쿼리(query), 키(key), 밸류(value), 아웃풋(output) 프로젝션 레이어에 적용하죠.

LoRA 설정 후 학습 가능한 파라미터 수가 얼마나 줄었는지 확인하는 모습입니다. 정말 극적으로 줄어들죠?

5. 트레이너 설정 및 모델 학습

이제 trl 라이브러리의 SFTTrainer를 사용해서 모델 학습을 시작합니다. SFTTrainer는 지도 파인튜닝(Supervised Fine-Tuning)에 특화되어 있어서 데이터셋 처리가 정말 편해요.

from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset

# 데이터셋 로드 (위에서 만든 example_dataset.json 사용)
dataset = load_dataset("json", data_files="example_dataset.json")

# 훈련 인자 설정
training_args = TrainingArguments(
    output_dir="./results", # 결과 저장 디렉토리
    num_train_epochs=3, # 학습 에포크 수
    per_device_train_batch_size=2, # GPU당 배치 크기 (메모리 제약 시 줄임)
    gradient_accumulation_steps=4, # 기울기 누적 스텝 수 (가상 배치 크기 증가)
    optim="paged_adamw_8bit", # 8비트 AdamW 옵티마이저 (메모리 효율적)
    learning_rate=2e-4, # 학습률
    logging_steps=10, # 로깅 스텝
    save_strategy="epoch", # 에포크마다 모델 저장
    evaluation_strategy="no", # 평가 전략 (간단 예시에서는 평가 생략)
    fp16=False, # QLoRA 사용 시 FP16은 비활성화
    bf16=True, # bfloat16 사용 (A100, RTX 30/40 시리즈 등 지원 GPU)
)

# SFTTrainer 생성
trainer = SFTTrainer(
    model=model,
    train_dataset=dataset["train"],
    peft_config=lora_config,
    dataset_text_field="input", # 텍스트 필드 지정 (데이터셋 구조에 따라 다름)
    max_seq_length=512, # 최대 시퀀스 길이
    tokenizer=tokenizer,
    args=training_args,
)

# 학습 시작!
trainer.train()

# 학습된 어댑터 저장
trainer.model.save_pretrained("./my_llm_adapter")
tokenizer.save_pretrained("./my_llm_adapter")

gradient_accumulation_steps는 메모리가 부족할 때 배치 크기를 효과적으로 늘리는 방법이에요. per_device_train_batch_size * gradient_accumulation_steps가 실제 효과적인 배치 크기가 됩니다. bf16=True는 bfloat16을 지원하는 GPU에서 모델 학습 속도를 높이고 메모리 효율을 좋게 만듭니다.

⚠️ 주의사항 및 트러블슈팅 (삽질 경험 공유)

제가 이 과정에서 정말 많이 겪었던 문제들이 몇 가지 있어요. 독자 여러분은 저처럼 삽질하지 마시라고 공유해드립니다!

  • ⚠️ CUDA Out of Memory (OOM) 에러: 가장 흔하게 만나는 문제일 거예요. 특히 GPU 메모리가 8GB, 12GB 정도라면 QLoRA를 써도 OOM이 발생할 수 있어요.
    • 해결책: per_device_train_batch_size를 1로 줄이고, gradient_accumulation_steps를 늘려보세요. max_seq_length도 줄이는 것이 도움이 될 수 있습니다. bitsandbytesbnb_4bit_compute_dtypetorch.bfloat16 대신 torch.float16으로 바꿔보는 것도 방법입니다. (단, bf16=True 대신 fp16=True로 설정해야 합니다.)
  • ⚠️ 모델 학습 속도 저하: QLoRA는 메모리 효율적이지만, 4비트 양자화된 가중치를 매번 역양자화(dequantize)해서 계산해야 하므로 학습 속도가 약간 느려질 수 있어요.
    • 해결책: bf16=True를 사용하면 (지원 GPU 한정) 속도 개선에 도움이 됩니다. 데이터 로딩 파이프라인 최적화(예: num_workers 설정)도 고려해보세요.
  • ⚠️ 데이터셋 포맷 오류: SFTTrainerdataset_text_fieldformatting_func 설정이 데이터셋 구조와 맞지 않으면 에러가 납니다.
    • 해결책: 데이터셋 JSON 파일의 키(key)와 dataset_text_field가 정확히 일치하는지 확인해야 해요. 만약 복잡한 포맷이라면 formatting_func를 직접 구현해서 데이터를 원하는 형태로 만들어줘야 합니다. 저도 여기서 한참 헤맸거든요.
  • ⚠️ 커스텀 모델의 성능 부족: 아무리 파인튜닝을 해도 원하는 결과가 나오지 않을 때가 있어요.
    • 해결책: 가장 큰 원인은 데이터셋의 품질과 양입니다. 충분히 다양하고 고품질의 데이터가 아니라면 모델은 제대로 학습되지 않아요. LoRA 하이퍼파라미터(r, lora_alpha, learning_rate)를 튜닝해보는 것도 중요합니다. 저는 wandb(Weights & Biases) 같은 툴을 써서 실험 결과를 추적하며 모델 학습의 최적 파라미터를 찾곤 합니다.

파인튜닝 모델 검증 및 결과 확인 🎉

자, 이제 학습이 끝났으니 우리가 만든 커스텀 LLM이 얼마나 잘 작동하는지 확인해볼 차례예요!

1. 학습된 모델 로드

학습된 LoRA 어댑터와 토크나이저를 다시 불러옵니다. 이때 원본 모델도 함께 로드해야 해요.

from transformers import AutoTokenizer, AutoModelForCausalLM
from peft import PeftModel, PeftConfig
import torch

# 원본 모델 로드 (QLoRA 설정과 동일하게)
model_id = "meta-llama/Llama-2-7b-hf"
bnb_config = BitsAndBytesConfig(
    load_in_4bit=True,
    bnb_4bit_use_double_quant=True,
    bnb_4bit_quant_type="nf4",
    bnb_4bit_compute_dtype=torch.bfloat16
)
base_model = AutoModelForCausalLM.from_pretrained(
    model_id,
    quantization_config=bnb_config,
    device_map="auto"
)
tokenizer = AutoTokenizer.from_pretrained(model_id)

# 학습된 LoRA 어댑터 로드
peft_model_id = "./my_llm_adapter"
model = PeftModel.from_pretrained(base_model, peft_model_id)

# 모델을 평가 모드로 전환 (Dropout 등 비활성화)
model.eval()

2. 추론 (Inference)

이제 우리의 커스텀 모델에 질문을 던져봅시다. 학습 데이터셋에 있던 내용과 비슷한 질문을 던져보면, 훨씬 더 정확하고 원하는 형식의 답변을 생성하는 것을 볼 수 있을 거예요.

# 질문 생성 (데이터셋 형식과 유사하게)
prompt = "### Instruction:\n다음 질문에 대해 간결하게 답변해 주세요.\n\n### Input:\nLLM 파인튜닝은 무엇인가요?\n\n### Output:"

# 토크나이저로 인코딩
inputs = tokenizer(prompt, return_tensors="pt").to("cuda")

# 모델로 답변 생성
with torch.no_grad():
    outputs = model.generate(
        **inputs,
        max_new_tokens=200, # 최대 생성 토큰 수
        do_sample=True, # 샘플링 기반 생성
        top_p=0.9, # 상위 p 확률 내에서 샘플링
        temperature=0.7, # 창의성 조절
    )

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

출력된 답변을 보면, 우리가 파인튜닝한 의도대로 간결하고 정확한 정보가 나오는 것을 확인할 수 있어요. 처음엔 정말 너무 기특해서 박수까지 쳤다니까요! 🎉

파인튜닝된 LLM이 질문에 대해 답변하는 모습입니다. 우리가 의도한 대로 잘 대답하고 있죠?

마무리 💡

오늘은 13년차 인프라 엔지니어인 제가 직접 경험하며 익힌 LLM 파인튜닝의 세계, 특히 LoRAQLoRA 기법을 활용한 커스텀 LLM 구축 방법에 대해 자세히 알아봤습니다.

이 방법들을 통해 우리는 다음과 같은 장점을 얻을 수 있더라고요.

  • ✅ GPU 메모리 효율성: QLoRA 덕분에 적은 GPU 자원으로도 거대 모델을 파인튜닝할 수 있게 되었습니다. 저처럼 홈랩을 운영하는 분들에게는 정말 큰 축복이에요!
  • ✅ 빠른 모델 학습: 전체 모델을 학습하는 대신 소수의 파라미터만 학습하여 시간을 절약할 수 있습니다.
  • ✅ 저장 및 배포 용이성: 학습된 LoRA 어댑터는 크기가 매우 작아서 관리하고 공유하기가 훨씬 편해요.

물론, 이 과정에서 수많은 삽질과 시행착오가 있었지만, 결국 원하는 결과를 얻었을 때의 뿌듯함은 정말 대단했습니다. 여러분도 저의 경험이 시행착오를 줄이는 데 도움이 되기를 바라요.

이제 여러분은 나만의 특화된 LLM을 만들 수 있는 강력한 도구를 손에 넣으신 거예요. 다음 단계로는 더 다양한 데이터셋으로 실험해보거나, Mistral, Gemma 같은 다른 오픈소스 LLM에 적용해보는 것도 정말 좋은 도전이 될 거예요. 궁금한 점이나 추가로 다루었으면 하는 주제가 있다면 언제든지 댓글로 남겨주세요! 다음 글에서는 이렇게 파인튜닝된 모델을 실제 서비스에 배포하는 과정에 대해 다뤄볼까 합니다. 기대해주세요!

LoRA와 QLoRA의 주요 장점과 고려 사항을 요약한 인포그래픽입니다.