마루옵스서버 · 네트워크 · AI 기술 노트

소개문의

[Local LLM 구축하기 28] LoRA·QLoRA와 작은 학습 코드의 구조

TSUBAME 4.0 서버의 케이블과 연결부

참고 사진 · TSUBAME 4.0 서버의 케이블과 연결부 · 사진: Fukumoto / 원본 / CC BY-SA 4.0 · 목록용 크기 조정

Local LLM 구축하기 · 28/30 · 심화와 운영

전체 연재와 학습 순서 · 실습 코드 ZIP

새 문서를 답변에 반영하려면 RAG로 해결할 수 있는지 먼저 살펴봅니다. 반복되는 답변 형식이나 분류 행동을 바꾸려는 경우라면 미세조정을 검토할 수 있어요. 여기서는 LoRA·QLoRA의 원리를 읽고 작은 학습 코드의 연결 구조를 확인합니다.

선택 실습으로 CUDA를 사용할 수 있는 WSL/Linux와 별도 Python 환경이 필요합니다. Windows API 실습용 가상환경과는 분리합니다.

LoRA·QLoRA의 변화량 학습

LoRA는 기존 가중치 W를 고정하고 변화량을 두 작은 행렬의 곱 ΔW=BA로 표현합니다. d×k 전체 대신 랭크 r의 r(d+k)개 파라미터를 학습하는 방식입니다. 학습할 수는 줄지만 작은 r로 모든 작업을 충분히 표현할 수 있는 것은 아닙니다.

QLoRA는 양자화한 기반 모델을 고정하고 LoRA 어댑터를 학습합니다. 논문에서는 NF4와 이중 양자화 등을 사용합니다. 추론용 4비트 파일을 임의로 골라 그대로 학습하는 절차와는 다릅니다. 예제는 Transformers·PEFT·bitsandbytes가 지원하는 경로를 사용합니다.

학습 자료와 평가 분리

원문의 사실을 찾아 답하는 작업이라면 17편의 RAG부터 확인해 보세요. 미세조정이 필요한지는 형식, 말투, 분류 같은 반복 행동의 요구를 따져 봅니다. 학습 자료는 개인정보와 저작권, 학습 이용 권한을 확인한 것만 사용합니다.

train.jsonl은 가상의 영어 문답 세 개이고, 5스텝 학습은 연결을 살피기 위한 smoke test입니다. 한국어 능력이나 실무 성능 개선을 확인하는 데이터셋으로 사용할 수 없습니다. 손실이 줄어도 별도의 검증·평가 자료에서 확인해야 합니다.

선택 실습: CUDA에서 5스텝 연결 점검

이 글의 학습 환경은 CUDA 지원 WSL/Linux와 Python 3.12입니다. 기존 Windows API 가상환경을 재사용하지 않습니다. PyTorch 공식 설치 선택기로 맞는 CUDA 패키지를 설치한 뒤 아래 명령에서 GPU가 보이는지 확인합니다.

python -c "import torch; print(torch.__version__, torch.cuda.is_available())"
python -m pip install transformers peft accelerate bitsandbytes datasets

train_qlora.py와 train.jsonl을 같은 폴더에 둡니다. 필요한 메모리는 GPU와 패키지 버전, 시퀀스 길이에 따라 달라집니다. 이 예제에서 확인한 범위는 문법과 데이터 흐름까지입니다. CUDA 장비에서 학습을 실행한 결과는 별도로 확인해야 하며 특정 VRAM 용량의 성공을 보장하지 않습니다.

import json
from pathlib import Path
import torch
from datasets import Dataset
from transformers import (AutoTokenizer, AutoModelForCausalLM,
    BitsAndBytesConfig, TrainingArguments, Trainer, DataCollatorForSeq2Seq)
from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training

if not torch.cuda.is_available():
    raise SystemExit("This optional example requires a CUDA-capable PyTorch environment.")
base = "TinyLlama/TinyLlama-1.1B-Chat-v1.0"
compute = torch.bfloat16 if torch.cuda.is_bf16_supported() else torch.float16
quant = BitsAndBytesConfig(load_in_4bit=True, bnb_4bit_quant_type="nf4",
    bnb_4bit_use_double_quant=True, bnb_4bit_compute_dtype=compute)
tokenizer = AutoTokenizer.from_pretrained(base)
tokenizer.pad_token = tokenizer.eos_token
model = AutoModelForCausalLM.from_pretrained(base,
    quantization_config=quant, device_map={"": 0}, dtype=compute)
model.config.use_cache = False
model = prepare_model_for_kbit_training(model)
model = get_peft_model(model, LoraConfig(r=8, lora_alpha=16,
    target_modules=["q_proj", "v_proj"], lora_dropout=0.05,
    bias="none", task_type="CAUSAL_LM"))
rows = [json.loads(line) for line in
    Path(__file__).with_name("train.jsonl").read_text(encoding="utf-8").splitlines() if line.strip()]
def encode(row):
    messages = row["messages"]
    prefix = tokenizer.apply_chat_template(messages[:-1], tokenize=False,
                                           add_generation_prompt=True)
    full = tokenizer.apply_chat_template(messages, tokenize=False)
    result = tokenizer(full, add_special_tokens=False, truncation=True, max_length=256)
    n = len(tokenizer(prefix, add_special_tokens=False)["input_ids"])
    result["labels"] = [-100] * min(n, len(result["input_ids"])) + result["input_ids"][n:]
    if all(x == -100 for x in result["labels"]):
        raise ValueError("No answer tokens left after truncation")
    return result
original = Dataset.from_list(rows)
data = original.map(encode, remove_columns=original.column_names)
args = TrainingArguments(output_dir="adapter-smoke", max_steps=5,
    per_device_train_batch_size=1, gradient_accumulation_steps=1,
    learning_rate=2e-4, gradient_checkpointing=True, logging_steps=1,
    save_strategy="no", report_to="none", bf16=compute == torch.bfloat16,
    fp16=compute == torch.float16, optim="paged_adamw_8bit")
model.print_trainable_parameters()
Trainer(model=model, args=args, train_dataset=data,
    data_collator=DataCollatorForSeq2Seq(tokenizer, padding=True,
                                       label_pad_token_id=-100)).train()
model.save_pretrained("adapter-smoke")
tokenizer.save_pretrained("adapter-smoke")
python train_qlora.py
python -m pip freeze > training-requirements.txt

어댑터와 학습 전후 평가

adapter-smoke에는 어댑터와 토크나이저 설정 등이 저장됩니다. 이것만으로 기반 모델을 대체할 수는 없습니다. 추론할 때 같은 기반 모델과 호환되는 적재 경로가 필요합니다. GGUF 변환과 양자화도 별도 단계이며 구조와 어댑터에 따라 지원이 다릅니다.

미세조정 전후에 같은 평가 질문을 사용합니다. 새 형식은 따르지만 일반 질문에 약해졌는지, 훈련 문장을 그대로 복제하는지도 확인해 보세요. 보지 않은 예시에서 개선이 없다면 학습 시간을 늘리기 전에 자료와 작업 정의를 다시 살펴봅니다.

참고 자료

문서·모델 태그 확인 기준: 2026년 10월 18일. 기본 실습 환경: CUDA를 사용할 수 있는 WSL/Linux의 별도 학습 환경.


댓글

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

닉네임·이메일·댓글 내용·IP 주소·브라우저 정보는 댓글 게시 및 관리에 사용됩니다. 이메일과 접속 정보는 공개 댓글에 표시하지 않습니다. 개인정보 요청은 운영자 이메일로 보내 주세요. 개인정보처리방침