대규모 언어 모델(LLM)을 파인튜닝하려면 엄청난 계산 비용과 메모리 자원이 필요하게 됩니다. 회사에서 LLM을 활용하여 파인튜닝을 구현해본 경우, 정말 몸소 느끼게 되는 경험이었습니다. 특히, 기존의 풀 파인튜닝(Full Fine-Tuning) 방식은 모델 전체 가중치를 업데이트해야 하기 때문에 GPU 메모리 사용량이 많고 훈련 속도가 느려지는 문제가 있습니다. 이를 해결하기 위해 등장한 기술이 LoRA(Low-Rank Adaptation)와 QLoRA(Quantized LoRA)입니다. 두 가지 방법 모두 모델의 모든 가중치를 변경하지 않고, 일부를 효율적으로 업데이트하는 방식으로 메모리 사용량을 줄이면서도 높은 성능을 유지할 수 있습니다.
1. LoRA(Low-Rank Adaptation) 적용 방식
LoRA는 기존의 모델 가중치를 고정한 채, 가중치 변화량을 저차원 행렬(Low-Rank Matrix)로 근사하여 학습하는 방식입니다. 이렇게 하면 모델의 원래 파라미터를 수정하지 않으면서도 특정 태스크에 맞게 적응시킬 수 있습니다.
LoRA의 기본 개념은 아래와 같습니다.
- 모델의 특정 레이어(보통 어텐션 가중치)에 대해, 작은 저차원 행렬(A, B)을 추가합니다.
- 기존 가중치는 변경하지 않고, 추가된 저차원 행렬만 학습합니다.
- 훈련이 끝난 후, LoRA 모듈을 기존 모델 위에 적용하여 인퍼런스를 수행합니다.
이 방식을 사용하면 전체 모델의 업데이트가 필요하지 않기 때문에 GPU 메모리 사용량이 크게 줄어들고, 훈련 속도가 빨라지게 됩니다.
Hugging Face의 `peft` 라이브러리를 활용한 LoRA 적용 예제를 한 번 살펴보겠습니다.
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import LoraConfig, get_peft_model
# 모델 및 토크나이저 로드
model_name = "meta-llama/Llama-2-7b"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name)
# LoRA 설정
lora_config = LoraConfig(
r=8, # Rank 크기
lora_alpha=32, # Scaling Factor
target_modules=["q_proj", "v_proj"], # LoRA를 적용할 레이어
lora_dropout=0.1
)
# LoRA 모델 생성
lora_model = get_peft_model(model, lora_config)
# 모델 훈련 수행
lora_model.train()
LoRA의 장점은 훈련해야 할 파라미터 수가 크게 줄어들어 메모리를 절약할 수 있다는 점입니다. 또한, LoRA 모듈을 여러 개 저장하고 쉽게 교체할 수 있어, 같은 기본 모델을 사용하면서도 다양한 태스크에 빠르게 적용할 수 있습니다.
2. QLoRA(Quantized LoRA) 적용 방식
QLoRA는 LoRA를 기반으로 하지만, 추가적으로 모델 가중치를 4비트(4-bit)로 양자화(Quantization)하여 더 많은 메모리를 절약할 수 있도록 설계된 방식입니다. LoRA와 마찬가지로 모델의 원래 가중치는 변경하지 않으며, LoRA 모듈만 학습합니다. 하지만 QLoRA는 기존 LoRA보다 훨씬 적은 GPU 메모리를 사용하면서도 성능 저하를 최소화할 수 있습니다.
QLoRA의 핵심 개념은 아래와 같습니다.
- 모델의 모든 가중치를 4비트 정밀도로 양자화하여 저장합니다.
- 양자화된 모델 위에 LoRA 모듈을 추가하여 일부 가중치를 훈련합니다.
- 훈련 중에 필요할 때만 가중치를 높은 정밀도로 복원하여 연산을 수행합니다.
QLoRA를 사용하면 기존 LoRA보다 더 작은 GPU 메모리로 훈련이 가능하며, 대규모 모델도 단일 GPU에서 파인튜닝할 수 있습니다.
Hugging Face의 `bitsandbytes` 라이브러리를 활용한 QLoRA 적용 예제 또한 아래 코드로 살펴보겠습니다.
import torch
from transformers import AutoModelForCausalLM, AutoTokenizer, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model
# 4-bit 양자화 설정
quantization_config = BitsAndBytesConfig(
load_in_4bit=True, # 4-bit 양자화 활성화
bnb_4bit_compute_dtype=torch.float16 # 연산 시 사용할 데이터 타입
)
# 모델 및 토크나이저 로드 (4-bit 양자화 적용)
model_name = "meta-llama/Llama-2-7b"
tokenizer = AutoTokenizer.from_pretrained(model_name)
model = AutoModelForCausalLM.from_pretrained(model_name, quantization_config=quantization_config)
# LoRA 설정
lora_config = LoraConfig(
r=8,
lora_alpha=32,
target_modules=["q_proj", "v_proj"],
lora_dropout=0.1
)
# QLoRA 모델 생성
qlora_model = get_peft_model(model, lora_config)
# 모델 훈련 수행
qlora_model.train()
QLoRA의 가장 큰 장점은 대규모 모델을 상대적으로 적은 리소스로 학습할 수 있다는 점입니다. 일반적으로 65억(6.5B) 파라미터 이상의 모델은 2~4개의 GPU가 필요하지만, QLoRA를 사용하면 24GB VRAM이 있는 단일 GPU에서도 학습이 가능합니다.
그렇다면 LoRA vs QLoRA, 어떤 방식이 더 적합할까?
두 방법 모두 기존의 풀 파인튜닝 방식보다 훨씬 적은 리소스로 훈련할 수 있도록 설계되었지만, 사용 목적과 환경에 따라 적절한 방식을 선택해야 합니다.
LoRA는 상대적으로 GPU 메모리가 충분한 환경에서 빠르게 모델을 튜닝할 때 적합합니다. 기존 모델의 원래 가중치를 변경하지 않고 특정 태스크에 적응시키는 것이 목적이라면 LoRA가 효과적입니다. 특히, 하나의 모델을 여러 용도로 활용해야 할 경우, LoRA 모듈을 변경하는 방식으로 쉽게 적용할 수 있습니다.
반면, QLoRA는 극한의 메모리 최적화가 필요한 환경에서 유리합니다. 4-bit 양자화를 적용함으로써 대규모 모델도 단일 GPU에서 훈련할 수 있으며, 비용 효율성이 뛰어납니다. 하지만 양자화 과정에서 약간의 정밀도 손실이 발생할 수 있어, 고정밀 연산이 필요한 경우에는 LoRA보다 성능이 낮아질 가능성이 있습니다.
결론적으로, 메모리와 연산 자원이 충분한 환경이라면 LoRA를, 제한된 GPU 자원에서 대규모 모델을 다뤄야 한다면 QLoRA를 선택하는 것이 적절하다고 생각합니다. 최근에는 두 가지의 기법을 혼합하여 LoRA의 장점과 QLoRA의 메모리 절약 기능을 동시에 활용하는 시도도 이루어지고 있습니다.