소비자 GPU 한 장에서 QLoRA로 도메인 특화 SLM을 파인튜닝하는 방법
GPT-4o API 비용이 쌓이기 시작하는 순간, 자연스럽게 "이걸 우리 도메인에 맞게 직접 학습시킬 수 없을까?"라는 생각이 듭니다. 저도 그랬습니다. 의료 기록 요약 파이프라인에서 GPT-4를 쓰다가 월말 청구서를 보고 식겁한 게 계기였는데, 그러다 파인튜닝된 Qwen3-4B 하나가 특정 도메인 태스크에서 훨씬 큰 범용 모델을 넘기는 벤치마크 결과를 보고 진지하게 파고들기 시작했습니다.
잘 파인튜닝된 SLM은 특정 도메인 태스크에서 수십 배 큰 범용 LLM을 능가할 수 있으며, 추론 비용은 1/10~1/100 수준입니다. 2026년 기준으로 RTX 4070 Ti 한 장과 반나절이면 7B 모델을 도메인 전문가로 탈바꿈시킬 수 있습니다. 이 글에서는 핵심 개념부터 실제로 실행 가능한 코드, 그리고 직접 겪어온 시행착오까지 풀어봅니다. 글을 읽고 나면 Unsloth + QLoRA 조합으로 첫 번째 도메인 특화 SLM을 직접 학습시킬 수 있는 기반이 생길 겁니다.
핵심 개념
SLM이란 무엇이고, 왜 지금인가
SLM(Small Language Model)은 일반적으로 1B~13B 파라미터 수준의 소형 언어 모델을 가리킵니다. 이 정의 자체는 단순하지만, 중요한 건 "작다"는 게 더 이상 "못하다"를 뜻하지 않는다는 점입니다.
2026년 현재 SLM 파인튜닝 생태계가 흥미로운 이유는 세 가지입니다. 하드웨어 문턱이 급격히 낮아졌고(RTX 4070 Ti 한 장이면 충분합니다), 베이스 모델 품질이 크게 올라왔으며, 프레임워크 성숙도가 실무 사용에 충분한 수준에 도달했습니다. GlobalData는 2030년까지 SLM 시장이 207억 달러 규모, 연평균 15.1% 성장할 것으로 전망합니다.
SLM vs LLM: 규모 차이만이 아닌 사용 전략의 차이입니다. LLM은 범용 추론에 강하고, 파인튜닝된 SLM은 특정 도메인 태스크에서 LLM을 이깁니다. 도구를 선택하는 것이지, 우열을 가리는 게 아닙니다.
파인튜닝 방법론 3가지: 뭘 골라야 할까
처음 파인튜닝을 알아볼 때 SFT, LoRA, QLoRA가 동시에 등장해서 저도 한동안 헷갈렸습니다. 핵심은 메모리 제약과 허용 가능한 성능 트레이드오프에 따라 선택한다는 것입니다.
| 방법 | 작동 원리 | VRAM 요구 | 파국적 망각 위험 |
|---|---|---|---|
| SFT (Supervised Fine-Tuning) | 전체 가중치 업데이트 | 매우 높음 (40GB+) | 높음 |
| LoRA | 핵심 레이어에 소형 학습 행렬 삽입, 원본 동결 | 중간 (16~24GB) | 낮음 |
| QLoRA | LoRA + 4-bit 양자화 | 낮음 (8~12GB) | 낮음 |
여기서 양자화(Quantization) 란, 모델 가중치를 기존 32-bit나 16-bit 부동소수점 대신 4-bit 정수로 압축해 저장하는 기법입니다. 정보 손실이 일부 발생하지만, VRAM 사용량이 극적으로 줄어들어 소비자 GPU에서도 7B 모델을 올릴 수 있게 됩니다. QLoRA는 이 양자화 위에 LoRA를 얹은 조합으로, 현실적인 온프레미스 환경에서의 최선의 출발점입니다.
파국적 망각(Catastrophic Forgetting): 도메인 특화 데이터로 파인튜닝할 때 모델이 기존에 갖고 있던 범용 추론 능력을 잃어버리는 현상입니다. LoRA는 원본 가중치를 동결하고 소형 행렬만 학습하기 때문에 full SFT보다 이 현상이 훨씬 덜 발생합니다.
LoRA의 수학적 직관
복잡하게 생각할 필요 없습니다. 원본 가중치 행렬 W를 직접 업데이트하는 대신, 훨씬 작은 두 행렬 A와 B의 곱으로 변화량을 근사합니다. 여기서 r이 바로 rank입니다.
W_new = W_original + (B × A) × α/r이 수식이 무섭게 보여도 의미는 단순합니다: "원본 가중치는 건드리지 말고, 작은 행렬 두 개만 학습하자." rank가 작을수록 학습 파라미터가 줄고 VRAM이 절약되지만, 표현력도 줄어듭니다. 실제로 rank를 4로 낮췄더니 수렴이 불안정해지고, 32로 올렸더니 RTX 4070 Ti에서 VRAM이 모자랐습니다. 도메인 적응에는 r=16이 실질적인 시작점입니다.
lora_alpha도 짚고 넘어갈 부분입니다. alpha=16, r=16으로 설정하면 alpha/r = 1이 되어 LoRA 어댑터의 스케일링 팩터가 1로 고정됩니다. alpha와 r을 같은 값으로 맞추면 학습률과 LoRA의 실효 영향력이 직관적으로 일치하게 되어, 하이퍼파라미터 탐색 시 시작점을 단순하게 유지할 수 있습니다.
실전 적용
예시 1: QLoRA로 금융 도메인 SLM 학습시키기
Qwen3-4B를 금융 QA 태스크에 맞게 파인튜닝하는 기본 세팅입니다. Unsloth를 사용하면 동일한 작업을 HuggingFace Transformers만 쓸 때보다 약 2배 빠르고 VRAM을 70% 절감할 수 있습니다.
한 가지 짚어둘 함정이 있습니다. Qwen3-Instruct처럼 채팅 템플릿이 있는 모델을 파인튜닝할 때 데이터 형식이 결과에 큰 영향을 미칩니다. {"text": "질문\n답변"} 같은 단순 텍스트 형식은 모델이 기대하는 입력 구조와 달라 파인튜닝 효과가 크게 떨어집니다. tokenizer의 apply_chat_template을 활용해 모델에 맞는 형식으로 변환하는 것을 권장합니다.
from unsloth import FastLanguageModel
from trl import SFTTrainer
from transformers import TrainingArguments
from datasets import load_dataset
# 1. 모델 로드 (4-bit 양자화)
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="Qwen/Qwen3-4B-Instruct",
max_seq_length=2048,
dtype=None, # 자동 감지
load_in_4bit=True, # 4-bit 양자화(QLoRA) 활성화
)
# 2. LoRA 어댑터 설정
model = FastLanguageModel.get_peft_model(
model,
r=16, # rank: 표현력과 VRAM 절감의 균형점
target_modules=["q_proj", "v_proj", "k_proj", "o_proj"],
lora_alpha=16, # alpha/r = 1로 스케일링 팩터 중립화
lora_dropout=0.05,
bias="none",
use_gradient_checkpointing="unsloth",
)
# 3. 데이터셋 — Qwen3-Instruct 채팅 템플릿으로 변환
raw_dataset = load_dataset("json", data_files="finance_qa.jsonl")["train"]
def format_with_chat_template(example):
messages = [
{"role": "user", "content": example["question"]},
{"role": "assistant", "content": example["answer"]},
]
return {"text": tokenizer.apply_chat_template(messages, tokenize=False)}
dataset = raw_dataset.map(format_with_chat_template)
# 4. 학습
trainer = SFTTrainer(
model=model,
tokenizer=tokenizer,
train_dataset=dataset,
dataset_text_field="text",
max_seq_length=2048,
args=TrainingArguments(
per_device_train_batch_size=2,
gradient_accumulation_steps=4, # 실효 배치 크기 = 2 × 4 = 8, VRAM 유지
warmup_steps=50, # 초반 학습률을 서서히 올려 불안정 수렴 방지
num_train_epochs=3,
learning_rate=2e-4,
lr_scheduler_type="cosine",
fp16=True,
logging_steps=10,
output_dir="./finance-qwen3-4b",
report_to="wandb",
),
)
trainer.train()
# 5. LoRA 어댑터와 토크나이저 저장
model.save_pretrained("./finance-qwen3-4b-lora")
tokenizer.save_pretrained("./finance-qwen3-4b-lora")| 설정값 | 이유 |
|---|---|
r=16 |
도메인 적응의 표준 시작점, 표현력과 VRAM 절감의 균형 |
lora_alpha=16 |
alpha/r = 1로 스케일링 중립화, 학습률 효과를 직관적으로 제어 |
gradient_accumulation_steps=4 |
VRAM 유지하면서 실효 배치 크기를 4배로 늘리는 트릭 |
learning_rate=2e-4 |
LoRA 학습에 최적화된 범위, full SFT보다 높게 설정 가능 |
cosine 스케줄러 |
학습 후반 수렴 안정성 향상 |
warmup_steps=50 |
초기 학습률을 서서히 올려 초반 발산 방지 |
학습이 끝난 후, 저장된 LoRA 어댑터를 불러와 추론에 활용하는 방법은 다음과 같습니다.
from unsloth import FastLanguageModel
# 저장된 LoRA 어댑터 로드
model, tokenizer = FastLanguageModel.from_pretrained(
model_name="./finance-qwen3-4b-lora",
max_seq_length=2048,
dtype=None,
load_in_4bit=True,
)
FastLanguageModel.for_inference(model)
# 채팅 템플릿 적용 후 추론
messages = [{"role": "user", "content": "2025년 4분기 금리 전망은 어떻게 됩니까?"}]
inputs = tokenizer.apply_chat_template(
messages, return_tensors="pt", add_generation_prompt=True
).to("cuda")
outputs = model.generate(inputs, max_new_tokens=256, temperature=0.1)
response = tokenizer.decode(outputs[0][inputs.shape[-1]:], skip_special_tokens=True)
print(response)예시 2: RAG + SLM 하이브리드 파이프라인
파인튜닝만으로는 한계가 있습니다. 학습 데이터에 없는 최신 정보나 방대한 사내 문서를 모델이 알 수는 없으니까요. 그래서 형식과 전문 용어는 파인튜닝으로 주입하고, 최신 정보나 사내 데이터는 RAG로 보강하는 이중 전략을 팀에서도 선택했습니다. 두 역할을 파인튜닝 하나에 욱여넣으려다 실패하는 케이스를 꽤 많이 봤기 때문입니다.
아래는 LangChain v0.2+ 기준의 LCEL 방식으로 작성한 예시입니다. 이전 버전의 langchain.llms, langchain.vectorstores 경로는 langchain_community로 이동했고, RetrievalQA는 deprecated 상태이므로 아래의 방식을 권장합니다.
from langchain_community.llms import HuggingFacePipeline
from langchain_community.vectorstores import Chroma
from langchain_community.embeddings import HuggingFaceEmbeddings
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from transformers import pipeline
import torch
# 파인튜닝된 SLM 로드
slm_pipeline = pipeline(
"text-generation",
model="./finance-qwen3-4b-lora",
torch_dtype=torch.float16,
device_map="auto",
max_new_tokens=512,
)
llm = HuggingFacePipeline(pipeline=slm_pipeline)
# 사내 문서 벡터 DB (Chroma)
embeddings = HuggingFaceEmbeddings(model_name="BAAI/bge-m3")
vectorstore = Chroma(
persist_directory="./company_docs_db",
embedding_function=embeddings,
)
retriever = vectorstore.as_retriever(search_kwargs={"k": 3})
# LCEL 기반 RAG 체인
prompt = ChatPromptTemplate.from_template(
"""다음 컨텍스트를 참고해 질문에 답하세요.
컨텍스트:
{context}
질문: {question}"""
)
chain = (
{"context": retriever, "question": RunnablePassthrough()}
| prompt
| llm
| StrOutputParser()
)
result = chain.invoke("2025년 4분기 리스크 관리 기준은?")
print(result)이 패턴의 핵심은 역할 분리입니다. SLM은 "어떻게 답변하는지(형식, 용어, 톤)"를, RAG는 "무엇을 참조하는지(최신 정보, 사내 데이터)"를 담당합니다.
장단점 분석
장점
| 항목 | 내용 |
|---|---|
| 비용 효율 | 추론 비용이 GPT-4o 대비 1/10~1/100 수준, 트래픽이 많을수록 절감 효과 극대화 |
| 응답 지연시간 | 온프레미스 배포 시 API 호출 오버헤드 없음, 실시간 서비스에 유리 |
| 데이터 프라이버시 | 내부 인프라 완결 처리, 금융·의료·법무 등 규제 산업에서 필수 |
| 도메인 정확도 | 특정 도메인 태스크에서 수십 배 큰 범용 모델을 능가 가능 |
| 낮은 데이터 임계값 | 고품질 샘플 1,000~5,000개 수준으로 의미 있는 특화 달성 가능 (도메인·태스크 복잡도에 따라 편차 있음) |
단점 및 주의사항
| 항목 | 내용 |
|---|---|
| 파국적 망각 | 도메인 학습 시 범용 추론 능력 저하 가능 |
| 데이터 품질 의존성 | 편향되거나 대표성 낮은 데이터 → 오버피팅 |
| 평가 파이프라인 구축 | 도메인 특화 벤치마크 수립이 어려움 |
| 하이퍼파라미터 탐색 | rank, 타겟 레이어, 학습률 조합이 도메인마다 다름 |
| 범용 태스크 성능 저하 | 파인튜닝 후 일반 QA 능력이 떨어질 수 있음 |
파국적 망각은 LoRA를 사용하면 원본 가중치가 동결되므로 full SFT보다 훨씬 완화됩니다. 더 적극적으로 대응하고 싶다면, 파인튜닝 전 모델이 스스로 범용 대화 데이터를 생성해 보존용 데이터로 활용하는 SA-SFT 기법도 있습니다. 추가 외부 데이터 없이 적용 가능한 방식으로, 관심 있으시면 arXiv 2025 논문에서 자세히 확인해볼 수 있습니다.
실무에서 가장 흔한 실수
-
데이터 양에만 집착하는 것: 5,000개 이상부터는 수익 체감 구간에 들어갑니다. 10,000개를 무작정 모으기보다 1,000개를 꼼꼼하게 정제하는 편이 훨씬 효과적인 경우가 많습니다. 실제로 3,000개짜리 노이즈 많은 데이터셋보다 800개짜리 정제된 데이터셋이 더 나은 결과를 낸 경험이 있습니다.
-
평가 기준을 사후에 정하는 것: 학습 전에 "무엇이 개선되면 성공인가"를 정의해두지 않으면, 모델이 좋아진 건지 나빠진 건지 알 방법이 없습니다. ROUGE나 BERTScore 같은 자동 지표와 함께, 실제 출력을 읽어봤을 때의 품질 평가를 사전에 설계해두는 것을 권장합니다.
-
베이스 모델을 아무거나 고르는 것: 코딩·수학 태스크라면 Phi-4 Mini, 다국어·일반 NLP라면 Qwen3, 커뮤니티 자료가 중요하다면 Llama 3.x가 각각 유리합니다. 베이스 모델 선택이 파인튜닝 결과에 미치는 영향이 하이퍼파라미터 튜닝보다 크다는 것이 벤치마크에서도 일관되게 나타납니다.
마치며
SLM 파인튜닝은 더 이상 ML 연구자의 영역이 아닙니다. 도메인 데이터와 단일 GPU를 가진 개발팀이라면 누구나 접근할 수 있는 선택지입니다. 처음에는 개념이 많아 복잡하게 느껴지지만, 결국 QLoRA + 좋은 데이터 + 사전에 정의된 평가 파이프라인이라는 세 축에서 출발하면 됩니다. "데이터를 많이 모으면 해결된다"는 생각과 "학습 후에 평가 기준을 만들면 된다"는 생각, 이 두 가지를 먼저 내려놓는 것이 실제 성공률을 높이는 가장 빠른 길이었습니다.
지금 바로 시작해볼 수 있는 3단계:
-
베이스 모델과 데이터 준비: 목적에 맞는 베이스 모델(
Qwen3-4B-Instruct또는microsoft/Phi-4-mini-instruct)을 선택하고, 도메인 QA 쌍 1,000개를{"question": "...", "answer": "..."}형태의 JSONL 파일로 정제해볼 수 있습니다. 데이터 품질이 양보다 중요하므로, 모호하거나 틀린 답변은 미리 제거하는 것이 좋습니다. -
Unsloth + QLoRA로 첫 학습 실행:
pip install unsloth trl후 위의 예시 코드를 실행해볼 수 있습니다. RTX 4070 Ti 이상이면 배치 사이즈 2, gradient accumulation 4 조합으로 약 3~6시간 안에 3 에포크 학습이 완료됩니다. wandb를 함께 연결해두면 학습 곡선을 실시간으로 확인할 수 있습니다. -
도메인 벤치마크로 성능 비교: 학습 전 베이스 모델과 파인튜닝 후 모델을 동일한 20~50개 테스트 셋으로 평가해볼 수 있습니다. ROUGE-L이나 BERTScore 같은 자동 지표와 실제 출력 비교를 병행하면, 파인튜닝이 가져다주는 변화를 직접 확인할 수 있습니다.
참고 자료
필독 (시작점으로 권장)
- Fine-Tuning Small Language Models for Domain-Specific AI: An Edge AI Perspective | arXiv 2025
- We Benchmarked 12 SLMs Across 8 Tasks | distil labs
- EVAL #003: Fine-Tuning in 2026 — Axolotl vs Unsloth vs TRL vs LLaMA-Factory | DEV Community
심화 자료
- SLM Finetuning for Natural Language to Domain Specific Code Generation in Production | arXiv 2025
- Building Domain-Specific Small Language Models via Guided Data Generation | arXiv 2024
- Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide | Effloow
- Fine-Tuning Small Language Models with Domain-Specific Data On-Premises | SysArt
- Improved Supervised Fine-Tuning to Mitigate Catastrophic Forgetting | arXiv 2025
- SFT Doesn't Always Hurt General Capabilities | arXiv 2025
- 도메인 특화 LLM: Mistral 7B를 활용한 금융 업무분야 파인튜닝 | KCI
- Introduction to Small Language Models: The Complete Guide for 2026 | MachineLearningMastery
- How To Fine-Tune LLMs For Domain-Specific Adaptation | Open Source For You 2026
- A Practical Guide to Fine-Tuning Small Language Models | Omdena