QLoRA + 한국어 — T4 한 장으로 7B 모델을 한국어 전문가로 만들기

QLoRA + 한국어 — T4 한 장으로 7B 모델을 한국어 전문가로 만들기
Part 1에서 LoRA의 원리와 Qwen 2.5 7B 파인튜닝을 다뤘습니다. RTX 3090(24GB)에서 약 18GB VRAM이 필요했습니다. 이번 글에서는 QLoRA로 T4 16GB 한 장까지 줄이고, 한국어 데이터셋을 구축해서 실제로 한국어 응답 품질을 끌어올립니다.
시리즈: Part 1: LoRA 이론 | Part 2 (이 글) | Part 3: 평가 + 배포
QLoRA: 메모리의 한계를 뚫다
LoRA가 학습 파라미터를 99.8% 줄였다면, QLoRA는 모델 자체의 메모리까지 줄입니다.
모델 가중치를 4-bit로 양자화해서 올리고, LoRA 어댑터만 16-bit로 학습한다.
비유하자면: 도서관의 모든 책을 요약본(4-bit)으로 보관하되, 새로 쓰는 메모(LoRA)만 원본 해상도(16-bit)로 작성하는 겁니다. QLoRA 논문(Dettmers et al., 2023)은 세 가지 기술을 도입했습니다.
QLoRA의 세 가지 핵심 기술
1. 4-bit NormalFloat (NF4)
사전학습된 모델의 가중치는 정규분포를 따릅니다. 0 근처에 값이 밀집되어 있고 극단값은 드뭅니다. 일반 4-bit 양자화는 균등 간격으로 매핑하므로 이 분포에 맞지 않습니다.
NF4는 정규분포에 맞춰 0 근처에 양자화 레벨을 촘촘하게 배치합니다. 결과적으로 일반 INT4 대비 정보 손실이 절반 이하입니다.
2. Double Quantization
양자화를 할 때 각 블록(64개 가중치)마다 FP32 스케일링 상수가 필요합니다. 7B 모델에서 이 상수만 약 0.5GB. Double Quantization은 이 상수 자체를 다시 8-bit로 양자화해서 약 0.13GB로 줄입니다. 16GB GPU에서는 이 370MB가 학습 가능 여부를 결정합니다.
3. Paged Optimizers
학습 중 GPU 메모리가 부족하면 OOM 에러가 납니다. Paged Optimizers는 NVIDIA unified memory를 활용해서, optimizer 상태를 자동으로 CPU RAM으로 페이징합니다. OS의 가상 메모리와 같은 원리입니다.
메모리 비교: LoRA vs QLoRA
같은 Qwen 2.5 7B, 같은 설정(r=16, batch=2, seq_len=1024)으로 측정한 수치입니다.
LoRA에서는 RTX 3090이 필요했지만, QLoRA는 Colab 무료 티어의 T4에서도 돌아갑니다.
한국어 데이터셋 구축
모델을 한국어 전문가로 만들려면 고품질 한국어 instruction 데이터가 필요합니다. 1만 개의 엉성한 데이터보다 1,000개의 정제된 데이터가 훨씬 낫습니다.
데이터 형식: instruction-input-output
SFT(Supervised Fine-Tuning)에서 가장 표준적인 형식입니다.
{
"instruction": "다음 문장을 존댓말로 바꿔주세요.",
"input": "이거 빨리 해.",
"output": "이것을 빨리 해주시겠어요?"
}input은 선택적입니다. 없는 경우 instruction만으로 동작합니다:
{
"instruction": "대한민국의 수도는 어디인가요?",
"input": "",
"output": "대한민국의 수도는 서울특별시입니다."
}공개 한국어 데이터셋
이 글에서는 범용성과 크기가 균형 잡힌 kyujinpy/KOR-OpenOrca-Platypus-v3을 사용합니다.
데이터 품질 가이드라인
데이터를 직접 만들거나 필터링할 때 다음 기준을 적용하세요.
- 다양성: 요약, 번역, 코딩, 분석, 창작, 수학, 상식 등 최소 10개 카테고리. 편중되면 모델도 편향됩니다
- 응답 길이: 짧은 답변만 있으면 모델도 짧게만 대답합니다. 다양한 길이 혼합, 평균 토큰 150~300 권장
- 한국어 자연스러움: 기계 번역 투 제거("그것은 ~입니다" → "~입니다"), 영어 직역 교정, 존댓말 일관성 유지
QLoRA 실습 코드
T4 16GB에서 동작하는 전체 코드입니다.
환경 설정
!pip install -q transformers peft datasets accelerate bitsandbytes trl wandbimport torch
from transformers import AutoModelForCausalLM, AutoTokenizer, TrainingArguments, BitsAndBytesConfig
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training, TaskType
from datasets import load_dataset
from trl import SFTTrainer4-bit 양자화 설정
bnb_config = BitsAndBytesConfig(
load_in_4bit=True, # 4-bit 양자화 활성화
bnb_4bit_quant_type="nf4", # NormalFloat4 사용
bnb_4bit_compute_dtype=torch.bfloat16, # 계산은 BF16으로
bnb_4bit_use_double_quant=True, # Double Quantization
)코드의 인라인 주석이 각 파라미터를 설명합니다. 핵심은 저장은 4-bit, 연산은 BF16으로 한다는 점입니다.
모델 로드
model_name = "Qwen/Qwen2.5-7B-Instruct"
tokenizer = AutoTokenizer.from_pretrained(model_name)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(
model_name,
quantization_config=bnb_config,
device_map="auto",
)
# QLoRA를 위한 모델 준비 (gradient checkpointing + 입출력 레이어 FP32 변환)
model = prepare_model_for_kbit_training(model)prepare_model_for_kbit_training()은 gradient checkpointing 활성화, 입출력 레이어 FP32 유지, 양자화 레이어 freeze를 한 번에 처리합니다. 이 단계에서 VRAM은 약 4.2GB. Part 1의 14GB와 비교해보세요.
LoRA 설정
lora_config = LoraConfig(
task_type=TaskType.CAUSAL_LM,
r=16,
lora_alpha=32,
lora_dropout=0.05,
target_modules=["q_proj", "k_proj", "v_proj", "o_proj",
"gate_proj", "up_proj", "down_proj"],
)
model = get_peft_model(model, lora_config)
model.print_trainable_parameters()
# trainable params: 13,631,488 || all params: 3,947,921,408 || trainable%: 0.3452Part 1과 동일한 LoRA 설정입니다. 전체 파라미터 수가 7.6B → 3.9B로 표시되는 건 4-bit 양자화로 인한 메모리 기준 환산입니다.
한국어 데이터셋 로드 및 전처리
dataset = load_dataset("kyujinpy/KOR-OpenOrca-Platypus-v3", split="train")
dataset = dataset.shuffle(seed=42).select(range(3000)) # 3000개 샘플링def format_korean_chat(example):
"""한국어 instruction 데이터를 Qwen chat 형식으로 변환"""
system_msg = "당신은 한국어에 능통한 AI 어시스턴트입니다. 자연스럽고 정확한 한국어로 답변하세요."
user_content = example["instruction"]
if example.get("input") and example["input"].strip():
user_content += f"\n\n{example['input']}"
messages = [
{"role": "system", "content": system_msg},
{"role": "user", "content": user_content},
{"role": "assistant", "content": example["output"]},
]
text = tokenizer.apply_chat_template(messages, tokenize=False)
return {"text": text}
dataset = dataset.map(format_korean_chat)학습
training_args = TrainingArguments(
output_dir="./qwen25-qlora-korean",
num_train_epochs=2,
per_device_train_batch_size=2,
gradient_accumulation_steps=8, # 실질 배치 = 2 × 8 = 16
learning_rate=2e-4,
warmup_ratio=0.1,
weight_decay=0.01,
logging_steps=10,
save_strategy="steps",
save_steps=200,
save_total_limit=3,
bf16=True, # T4는 FP16도 가능: fp16=True
gradient_checkpointing=True,
optim="paged_adamw_8bit", # Paged Optimizer 사용
lr_scheduler_type="cosine",
report_to="wandb", # Wandb 연동
run_name="qwen25-qlora-korean-3k",
max_grad_norm=0.3, # gradient clipping
)
trainer = SFTTrainer(
model=model,
args=training_args,
train_dataset=dataset,
tokenizer=tokenizer,
dataset_text_field="text",
max_seq_length=1024,
packing=False,
)
trainer.train()Part 1과 달라진 핵심 설정들:
T4 16GB 기준 3,000개 데이터, 2 epoch 학습 시 약 2시간 30분이 소요됩니다.
저장 및 추론
# LoRA 어댑터 저장
model.save_pretrained("./qwen25-qlora-korean-adapter")
tokenizer.save_pretrained("./qwen25-qlora-korean-adapter")# 추론
from peft import PeftModel
base_model = AutoModelForCausalLM.from_pretrained(
model_name, quantization_config=bnb_config, device_map="auto",
)
model = PeftModel.from_pretrained(base_model, "./qwen25-qlora-korean-adapter")
messages = [
{"role": "system", "content": "당신은 한국어에 능통한 AI 어시스턴트입니다."},
{"role": "user", "content": "블록체인 기술을 비전공자에게 설명해주세요."},
]
inputs = tokenizer.apply_chat_template(messages, return_tensors="pt").to("cuda")
outputs = model.generate(inputs, max_new_tokens=512, temperature=0.7, top_p=0.9)
print(tokenizer.decode(outputs[0], skip_special_tokens=True))LoRA vs QLoRA 종합 비교
QLoRA가 30% 느린 이유는 연산마다 4-bit → BF16 역양자화가 필요하기 때문입니다. 하지만 30% 속도 저하로 44% 메모리를 절약하는 건 좋은 거래입니다. 품질 차이(98% vs 96%)는 벤치마크 기준이며, 한국어 instruction following에서는 데이터 품질이 양자화 손실보다 훨씬 큰 영향을 미칩니다.
Wandb로 학습 모니터링하기
loss가 줄어들지 않거나 갑자기 튀면 하이퍼파라미터를 조정해야 합니다. Wandb를 연결하면 실시간으로 확인할 수 있습니다.
import wandb
wandb.init(
project="qlora-korean-finetuning",
name="qwen25-7b-korean-3k",
config={
"model": "Qwen/Qwen2.5-7B-Instruct",
"quantization": "NF4",
"lora_r": 16,
"lora_alpha": 32,
"dataset_size": 3000,
"epochs": 2,
"learning_rate": 2e-4,
}
)report_to="wandb"를 설정했으므로, trainer.train() 호출 시 자동으로 train/loss, train/learning_rate, train/grad_norm이 기록됩니다.
3,000개 데이터, 2 epoch 학습 시 전형적인 loss 패턴:
Step 10: loss=2.45, lr=4.0e-05 ← warmup 구간
Step 50: loss=1.82, lr=1.8e-04 ← warmup 완료, 빠른 하락
Step 100: loss=1.35, lr=2.0e-04 ← 최대 학습률 도달
Step 200: loss=1.08, lr=1.7e-04 ← cosine 감소 시작
Step 300: loss=0.92, lr=1.2e-04 ← 안정적 하락
Step 375: loss=0.85, lr=5.0e-05 ← 학습 종료 근처loss가 1.0 아래로 내려가면 한국어 패턴을 잘 학습하고 있다는 신호입니다. 0.5 아래로 내려가면 과적합을 의심하세요. 학습 종료 후 wandb.finish()를 호출합니다.
트러블슈팅
다음 편 예고: 평가 + 배포
Part 3에서는:
- 한국어 벤치마크 평가: KoBEST, KLUE, 자체 평가셋으로 성능 측정
- vLLM 배포: 양자화 모델 + LoRA 어댑터를 프로덕션 서빙
- 어댑터 합치기(merge): base model과 LoRA를 하나로 합쳐서 추론 최적화
- 실전 팁: 학습률, rank, 데이터 크기의 최적 조합 가이드