Skip to main content
튜토리얼 시리즈 | Kubeflow 기반 LLM 워크플로우

2. LLM 모델 파인튜닝

📘 사전 학습(pretrained)모델을 파인튜닝하고, 추론까지 실습하는 전 과정을 안내합니다.

기본 정보
  • 예상 소요 시간: 60분
  • 권장 운영 체제: Ubuntu

시나리오 소개

Kubeflow 환경에서 Jupyter Notebook을 활용하여 카카오의 카나나(Kanana)Meta Llama 3.2의 사전 학습(pretrained)모델을 GPU 환경에서 파인튜닝하는 과정을 설명합니다. 사용자는 카카오클라우드 기술문서 데이터를 기반으로 도메인 특화된 LLM을 구축하고, 모델 저장 및 추론 테스트까지 실습할 수 있습니다.

주요 내용은 다음과 같습니다.

  • GPU 기반 노트북 인스턴스 생성
  • 학습 데이터 전처리 및 토큰화
  • PEFT 또는 Unsloth 기반 LLM 파인튜닝 수행
  • 튜닝된 모델 저장 및 Object Storage 업로드
  • 추론 테스트를 통한 모델 응답 결과 확인

지원 도구

도구버전설명
Jupyter Notebook4.2.1다양한 머신러닝 프레임워크와 Kubeflow SDK 연동을 지원하는 웹 기반 개발 환경
PEFTlatest사전 학습된 LLM에 효율적인 파인튜닝 수행을 위한 Huggingface 라이브러리
ㄴ Kanana 모델의 LoRA 파인튜닝에도 사용
Unsloth2025.3Llama3에 최적화된 QLoRA 기반 파인튜닝 도구

시작하기 전에

1. Kubeflow 환경 준비

Kubeflow에서 LLM 모델 파인튜닝을 수행하기 위해 아래 사양의 GPU 노드 환경이 필요합니다. 사전 준비 사항을 참고하여 GPU 노드 풀이 설정된 환경을 먼저 준비하세요.

2. Object Storage 버킷 생성

파인튜닝된 모델을 저장할 Object Storage 버킷을 생성합니다. 생성한 버킷은 이후 모델 서빙 시 모델 파일을 참조하는 경로로 사용됩니다.

  • 버킷 생성에 대한 자세한 내용은 Object Storage 버킷 생성 및 관리 문서를 참고해 주세요.
  • Kubeflow 생성 단계에서 이미 카카오클라우드 Object Storage를 선택한 경우, kubeflow-{kubeflow id} 형식의 자동 생성 버킷을 활용할 수 있습니다.

3. 학습 데이터세트 준비

카카오클라우드 기술문서의 Kubeflow 서비스 가이드와 튜토리얼 텍스트를 학습 데이터로 활용하여, GPU 환경에서 파인튜닝 과정을 단계별로 실습합니다. 아래에서 샘플 데이터를 다운로드해 주세요.

시작하기

본 시나리오에서는 카카오의 Kanana와 Meta Llama 3 기반의 사전 학습(pretrained) 모델에 대해 LoRA 또는 QLoRA 기법을 적용한 파인튜닝 방법을 설명합니다. LoRA는 Huggingface의 PEFT 라이브러리를 사용해 구현되며, QLoRA는 Llama3에 최적화된 Unsloth 도구를 통해 구현됩니다. Kanana 모델은 PEFT 및 Transformers와 호환되며, LoRA 기반 파인튜닝이 가능합니다.

두 방식 모두 파라미터 효율성과 낮은 리소스 사용을 강점으로 하며, 사용 환경과 목적에 따라 선택해 적용할 수 있습니다.

info

각 기법에 대한 자세한 설명은 Huggingface PEFT 공식 문서Unsloth 공식 문서를 확인하세요.

Step 1. Jupyter Notebook 인스턴스 생성

Kubeflow 대시보드에서 파인튜닝 실습을 위한 GPU 기반 노트북 인스턴스를 생성합니다.

  1. Kubeflow 대시보드에서 Notebooks 탭을 선택합니다.

  2. 우측 상단의 [New Notebook] 버튼을 클릭하여 새 인스턴스를 생성합니다.

  3. New notebook 설정 화면에서 다음 정보를 입력합니다.

    • Notebook Imagekc-kubeflow/jupyter-pytorch-cuda-full:v1.8.0.py311.1a 선택
    • Notebook 최소 사양: vCPU 3개 이상, Memory 6GB 이상 입력
    • GPU 연결: Number of GPUs 1개, GPU Vendor NVIDIA MIG - 1g.10gb 선택
  4. 설정을 입력한 후, [LAUNCH] 버튼을 클릭하여 인스턴스를 생성합니다.

Step 2. 패키지 설치 및 GPU 연결 확인

파인튜닝에 필요한 Python 패키지를 설치하고, GPU 리소스가 정상적으로 연결되어 있는지 확인합니다.

  1. 사용하려는 방식에 따라 아래 명령어를 선택하여 노트북에서 필요한 패키지를 설치하세요.

    ! pip install transformers peft datasets
  2. 아래 코드를 실행하여 GPU 드라이버 및 CUDA 환경이 올바르게 설정되었는지 확인합니다.

    Pytorch GPU 환경 확인
    import os
    import torch

    os.environ["NVIDIA_VISIBLE_DEVICES"] = "0"
    os.environ["CUDA_VISIBLE_DEVICES"] = "0"

    print(torch.__version__)
    print(torch.version.cuda)
    print(torch.backends.cudnn.version())
    print(torch.cuda.is_available())

    GPU가 정상적으로 연결되어 있다면 아래와 같이 True가 출력됩니다.

    출력 예시
    2.6.0+cu124
    12.4
    90100
    True

Step 3. Pre-trained 모델 로드

Hugging Face 모델 허브에서 지정한 model_name 경로를 통해 두 모델의 사전 학습(pre-trained) 파일을 다운로드하고 모델과 토크나이저를 로드합니다. 선택한 방식(PEFT 또는 Unsloth)에 따라 아래 명령어를 사용해 모델을 GPU 환경에 맞게 로드합니다.

import torch
from transformers import AutoModelForCausalLM, AutoTokenizer

# 튜닝할 기본 모델 로드
model_name = "kakaocorp/kanana-nano-2.1b-base" # 또는 "meta-llama/Llama-3.1-8B"
base_model = AutoModelForCausalLM.from_pretrained(
model_name,
torch_dtype=torch.bfloat16,
trust_remote_code=True
).to("cuda")

# 토크나이저 로드
tokenizer = AutoTokenizer.from_pretrained(model_name, padding_side="left")
# 토크나이저 설정: llama 타입 모델은 아래와 같이 pad_token 지정 필요
tokenizer.pad_token = tokenizer.eos_token

Step 4. 데이터세트 준비

제공된 CSV 형식의 학습 데이터를 노트북 환경에 업로드한 후, Huggingface Datasets 라이브러리를 사용해 데이터셋을 로드하고 LLM 학습에 적합한 포맷으로 전처리 및 토큰화를 수행합니다.

  1. sample_train_data.csv 파일을 로컬 노트북 환경에 업로드한 후, Huggingface Datasets 라이브러리를 이용해 로드합니다.

    CSV 파일 로드
    from datasets import Dataset

    dataset = Dataset.from_csv('sample_train_data.csv')
    dataset
    출력 예시
    Generating train split: 
    111/0 [00:00<00:00, 7271.09 examples/s]
    Dataset({
    features: ['Unnamed: 0', 'instruction', 'output', 'input'],
    num_rows: 111
    })
  2. LLM 모델 학습에 맞게 학습 데이터의 instruction-input-output 컬럼을 Alpaca 스타일의 프롬프트 형식으로 변환합니다.
    이를 위해 전처리 함수(formatting_prompts_func)를 를 정의하고, 데이터셋에 적용하여 모델 학습에 필요한 텍스트 입력 형태를 생성합니다.

    def formatting_prompts_func(examples):
    alpaca_prompt = """Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

    ### Instruction:
    {}

    ### Input:
    {}

    ### Response:
    {}"""

        instructions = examples["instruction"]
    inputs = examples["input"]
    outputs = examples["output"]

       EOS_TOKEN = tokenizer.eos_token # Must add EOS_TOKEN

    texts = []
    for instruction, input, output in zip(instructions, inputs, outputs):
    # Must add EOS_TOKEN, otherwise your generation will go on forever!
    text = alpaca_prompt.format(instruction, input, output) + EOS_TOKEN
    texts.append(text)
    return { "text" : texts, }

    dataset = dataset.map(formatting_prompts_func, batched = True,)
    dataset = dataset.remove_columns(['Unnamed: 0'])
    dataset
    출력 예시
    Map: 100% 111/111 [00:00<00:00, 5646.33 examples/s]
    Dataset({
    features: ['instruction', 'output', 'input', 'text'],
    num_rows: 111
    })
  3. LoRA 또는 QLoRA 기반 파인튜닝을 위해서는 카카오 Kanana-Nano-2.1B와 Meta Llama 3.2 모델 모두 토큰화된 입력 데이터셋이 필요합니다.
    아래 코드는 Hugging Face 토크나이저를 이용해 텍스트 데이터를 학습용 형식으로 변환하는 전처리 예시입니다.

    토큰화 전처리 함수
    def tokenize_function(examples):
    tokens = tokenizer(examples["text"], padding=True, return_tensors="pt")
    tokens["labels"] = tokens["input_ids"]
    return tokens

    dataset = dataset.map(tokenize_function, batched=True, remove_columns=["text"])

Step 5. 모델 학습

사전 학습된 LLM을 기반으로, LoRA 또는 QLoRA 방식으로 파인튜닝을 수행합니다. 이 단계에서는 모델 구조를 변환하고, 학습 설정을 구성한 뒤 실제로 학습을 실행합니다.

1. 모델 구조 변환 및 학습 설정

사용하는 방식(PEFT 또는 Unsloth)에 따라, 사전 학습된 모델을 LoRA 또는 QLoRA 형식으로 변환하고 필요한 학습 인자를 설정합니다. 아래 탭에서 각 방식의 구성 방법을 확인할 수 있습니다.

  1. PEFT 라이브러리의 get_peft_model 함수를 사용하여 LoRA 기반 PeftModel을 생성하고 파인튜닝을 수행합니다.

    from peft import LoraConfig, get_peft_model

    lora_config = LoraConfig(
    task_type="CAUSAL_LM",
    r=8,
    lora_alpha=32,
    lora_dropout=0.1,
    target_modules=[
    "q_proj",
    "k_proj",
    "v_proj"]
    )
    model = get_peft_model(base_model, lora_config)
  2. Transformers 라이브러리의 Trainer와 TrainingArguments 클래스를 사용하여 학습에 필요한 하이퍼파라미터를 설정합니다.

    output_dir모델 저장 경로
    num_train_epochs훈련 epoch 수
    per_device_train_batch_size각 장치의 배치 크기
    gradient_accumulation_steps그래디언트 축적 단계 (메모리 효율화)
    evaluation_strategy평가 전략 설정 ("no", "epoch", "steps")
    save_strategy저장 전략 설정 ("no", "epoch", "steps")
    save_steps저장 간격 (스텝 단위)
    learning_rate학습률
    weight_decay가중치 감소(weight decay) 설정
    logging_steps로깅 간격 (스텝 단위)
    fp16혼합정밀도 사용 여부 (CPU 환경에서는 False 권장)
    from transformers import Trainer, TrainingArguments

    trainer = Trainer(
    model=model,
    train_dataset=dataset,
    args=TrainingArguments(
    per_device_train_batch_size = 2,
    gradient_accumulation_steps = 4,
    warmup_steps = 5,
    max_steps = 60,
    learning_rate = 2e-4,
    bf16 = True,
    logging_steps = 1,
    weight_decay = 0.01,
    lr_scheduler_type = "linear",
    seed = 1234,
    output_dir = "outputs",
    report_to = "none"
    )
    )

2. 학습 전 GPU 메모리 확인

모델 학습을 시작하기 전에, 현재 GPU 환경의 메모리 상태를 출력하여 리소스 사용 상황을 확인합니다.

# 현재 메모리 상태를 보여주는 코드
gpu_stats = torch.cuda.get_device_properties(0) # GPU 속성 가져오기
start_gpu_memory = round(
torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3
) # 시작 시 예약된 GPU 메모리 계산
max_memory = round(
gpu_stats.total_memory / 1024 / 1024 / 1024, 3
) # GPU의 최대 메모리 계산
print(
f"GPU = {gpu_stats.name}. Max memory = {max_memory} GB."
) # GPU 이름과 최대 메모리 출력
print(f"{start_gpu_memory} GB of memory reserved.") # 예약된 메모리 양 출력
출력 예시
GPU = NVIDIA A100 80GB PCIe MIG 1g.10gb. Max memory = 9.5 GB.
9.252 GB of memory reserved.

3. 모델 학습 실행

설정된 Trainer 인스턴스를 사용하여 모델 학습을 실행합니다.

# 모델 훈련
trainer.train()
출력 예시
Step	Training Loss
1 0.441900
2 0.440900
3 0.372000
4 0.435600
5 0.281700
6 0.328600
7 0.300900
8 0.263100
9 0.319700
10 0.238000
11 0.296800
12 0.305000
13 0.211000
14 0.272500
15 0.150800
16 0.143300
17 0.179500
18 0.166800
19 0.138900
20 0.169000
21 0.174700
22 0.219700
23 0.185800
24 0.174700
25 0.153700
26 0.145200
27 0.160600
28 0.183200
29 0.105200
30 0.096700
31 0.105100
32 0.087900
33 0.073600
34 0.086600
35 0.079100
36 0.078400
37 0.091000
38 0.082200
39 0.086900
40 0.099400
41 0.121900
42 0.078300
43 0.061300
44 0.055300
45 0.053300
46 0.059000
47 0.050900
48 0.062600
49 0.057600
50 0.058700
51 0.053400
52 0.065500
53 0.061800
54 0.057600
55 0.077700
56 0.075800
57 0.054700
58 0.047000
59 0.043600
60 0.054600

4. 학습 후 상태 출력

모델 학습이 완료된 후, GPU 메모리 사용량 및 훈련에 소요된 시간을 출력합니다.

# 최종 메모리 및 시간 통계
used_memory = round(
torch.cuda.max_memory_reserved() / 1024 / 1024 / 1024, 3
) # 사용된 최대 메모리를 GB 단위로 계산
used_memory_for_lora = round(
used_memory - start_gpu_memory, 3
) # LoRA를 위해 사용된 메모리를 GB 단위로 계산
used_percentage = round(
used_memory / max_memory * 100, 3
) # 최대 메모리 대비 사용된 메모리의 비율을 계산
lora_percentage = round(
used_memory_for_lora / max_memory * 100, 3
) # 최대 메모리 대비 LoRA를 위해 사용된 메모리의 비율을 계산
print(
f"{trainer_stats.metrics['train_runtime']} seconds used for training."
) # 훈련에 사용된 시간을 초 단위로 출력
print(
# 훈련에 사용된 시간을 분 단위로 출력
f"{round(trainer_stats.metrics['train_runtime']/60, 2)} minutes used for training."
)
print(
f"Peak reserved memory = {used_memory} GB."
) # 예약된 최대 메모리를 GB 단위로 출력
print(
f"Peak reserved memory for training = {used_memory_for_lora} GB."
) # 훈련을 위해 예약된 최대 메모리를 GB 단위로 출력
print(
f"Peak reserved memory % of max memory = {used_percentage} %."
) # 최대 메모리 대비 예약된 메모리의 비율을 출력
print(
f"Peak reserved memory for training % of max memory = {lora_percentage} %."
) # 최대 메모리 대비 훈련을 위해 예약된 메모리의 비율을 출력
출력 예시
379.7191 seconds used for training.
6.33 minutes used for training.
Peak reserved memory = 9.252 GB.
Peak reserved memory for training = 0.0 GB.
Peak reserved memory % of max memory = 97.389 %.
Peak reserved memory for training % of max memory = 0.0 %.

Step 6. 모델 저장

튜닝이 완료된 모델을 Object Storage에 저장합니다. 저장된 모델은 추후 배포 또는 추론 과정에서 사용됩니다.

튜닝이 완료된 PEFT 모델은 LoRA 어댑터 가중치만 저장하거나, 사전 학습 모델과 병합하여 완전한 모델로 저장할 수 있습니다.

1. LoRA 어댑터 가중치만 저장
model_dir = "kanana-2-1b-kcdocs-adapt" #또는 "llama-3-8b-it-kcdocs-adapt"

model.save_pretrained(model_dir)
2. 사전 학습 모델과 병합하여 저장

Huggingface 기반 서빙에 사용할 경우, merge_and_unload()를 통해 병합한 모델과 토크나이저를 함께 저장합니다.

model_dir = "kanana-2-1b-kcdocs" #또는 "llama-3-8b-it-kcdocs"

# 기본 모델과 튜닝된 파라미터 병합 후 저장
model = model.merge_and_unload()

model.save_pretrained(model_dir)
# +huggingface 서빙 시 tokenizer 저장 필요
tokenizer.save_pretrained(model_dir)
3. 튜닝된 가중치만 적용해 불러오기

사전 학습 모델에 LoRA 가중치를 적용하는 방식으로 모델을 불러올 수도 있습니다. base model을 먼저 로드한 다음, 튜닝된 adapter 가중치를 적용합니다.

import torch
from transformers import AutoModelForCausalLM

from peft import PeftModel

# 1. 사전 학습 모델 로드
model_id = "kakaocorp/kanana-nano-2.1b-instruct" #또는 "unsloth/Meta-Llama-3.1-8B-bnb-4bit"

base_model = AutoModelForCausalLM.from_pretrained(
   model_id,
torch_dtype=torch.bfloat16,
trust_remote_code=True
).to("cuda")

adapter_dir = "kanana-2-1b-kcdocs-adapt" # 또는 "llama-3-8b-it-kcdocs-adapt"

# 2. 사전 학습 모델에 튜닝된 가중치 추가
model = PeftModel.from_pretrained(base_model, adapter_dir)
4. 병합 저장된 모델 직접 불러오기

저장한 병합 모델을 바로 로드하여 추론에 사용할 수 있습니다.

model_dir = "kanana-2-1b-kcdocs" # 또는 "llama-3-8b-it-kcdocs"

base_model = AutoModelForCausalLM.from_pretrained(
model_dir,
torch_dtype=torch.bfloat16,
trust_remote_code=True
).to("cuda")
5. Object Storage에 파일 업로드

튜닝된 모델 디렉토리(model_dir)의 모든 파일을 카카오클라우드 Object Storage 버킷에 업로드합니다.

안내
import boto3

# 발급받은 ec2 정보 입력
AWS_ACCESS_KEY_ID = '<YOUR EC2 CREDENTIAL ACCESS KEY>'
AWS_SECRET_ACCESS_KEY = '<YOUR EC2 CREDENTIAL SECRET KEY>'
ENDPOINT = "https://objectstorage.kr-central-2.kakaocloud.com"


# boto 클라이언트
client = boto3.client(
region_name="kr-central-2",
endpoint_url=ENDPOINT,
aws_access_key_id=AWS_ACCESS_KEY_ID,
aws_secret_access_key=AWS_SECRET_ACCESS_KEY,
service_name="s3"
)

# 버킷 생성 (필요시)
bucket_name = "kbm-llm-tutorial"
client.create_bucket(Bucket=bucket_name)


# 버킷에 파일 업로드
from glob import glob
file_paths = glob(f"{model_dir}/*")
object_names = [f.split("/")[-1] for f in file_paths]

for file_name, object_name in zip(file_paths, object_names):
print(file_name)
client.upload_file(
Filename=file_name,
Bucket=bucket_name,
Key=object_name
)

response = client.list_buckets()
출력 예시
./kbm-llm-tutorial/tokenizer_config.json
./kbm-llm-tutorial/tokenizer.json
./kbm-llm-tutorial/config.json
./kbm-llm-tutorial/model.safetensors
./kbm-llm-tutorial/generation_config.json
./kbm-llm-tutorial/special_tokens_map.json

dashboard1

Step 7. 학습 모델 추론

튜닝된 모델을 불러와 입력 문장에 대한 응답을 생성하는 추론 과정을 실행합니다. 다음 예시를 통해 모델의 응답을 검증할 수 있습니다.

예시 1: "AI 플랫폼을 추천해 주세요."

from transformers import TextStreamer

text_streamer = TextStreamer(tokenizer)

inputs = tokenizer(
[
alpaca_prompt.format(
"AI 플랫폼을 추천해주세요.", # 지시문(instruction)
"", # 입력(input) - 입력이 없는 경우 빈 문자열
"", # 출력(output) - 생성 용도이므로 비워 둡니다.
)
], return_tensors = "pt").to("cuda")

_ = model.generate(**inputs, streamer=text_streamer, max_new_tokens=2048, use_cache=False)
출력 예시
<bos>Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
AI 플랫폼을 추천해주세요.

### Input:


### Response:
카카오클라우드의 AI 서비스는 오픈소스 플랫폼 Kubeflow를 제공하여 머신러닝 워크플로우를 쉽게 구축하고 실행할 수 있도록 도와줍니다.<eos>
예시 2: "카카오클라우드에서 Kubeflow를 생성하는 방법을 알려 주세요."
inputs = tokenizer(
[
alpaca_prompt.format(
"카카오클라우드에서 Kubeflow를 생성하는 방법을 알려 주세요.", # instruction
"", # input
"", # output
)
], return_tensors = "pt").to("cuda")

_ = model.generate(**inputs, streamer=text_streamer, max_new_tokens=2048, use_cache=False)
출력 예시
<bos>Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
카카오클라우드에서 Kubeflow를 생성하는 방법을 알려 주세요.

### Input:


### Response:
카카오클라우드에서 Kubeflow를 생성하려면, 카카오클라우드 콘솔에서 Container Pack > Kubeflow를 클릭하고, [Kubeflow 생성]을 클릭하여 기본 설정과 쿼터 설정을 진행합니다. 그 후, Kubeflow 생성이 완료되면, kubectl 파일을 설정하고, Kubeflow 대시보드에 접속하여 사용자 네임스페이스를 생성합니다.<eos>

예시 3: "카카오클라우드 Kubeflow의 주요 기능을 알려 주세요."

inputs = tokenizer(
[
alpaca_prompt.format(
"카카오클라우드 Kubeflow의 주요 기능을 알려 주세요.", # instruction
"", # input
"", # output
)
], return_tensors = "pt").to("cuda")


_ = model.generate(**inputs, streamer=text_streamer, max_new_tokens=2048, use_cache=False)
출력 예시
<bos>Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
카카오클라우드 Kubeflow의 주요 기능을 알려 주세요.

### Input:


### Response:
카카오클라우드 Kubeflow는 데이터 전처리, 모델 학습, 모델 서빙 등을 위한 주요 기능을 제공하며, 일관된 인터페이스와 높은 수준의 추상화를 사용하여 개발자가 빠르고 쉽게 ML 모델을 개발할 수 있도록 돕습니다.<eos>

예시 4: "Kubeflow의 Katib에 대해 설명해 주세요."

inputs = tokenizer(
[
alpaca_prompt.format(
"Kubeflow의 Katib에 대해 설명해 주세요.", # instruction
"", # input
"", # output
)
], return_tensors = "pt").to("cuda")


_ = model.generate(**inputs, streamer=text_streamer, max_new_tokens=2048, use_cache=False)
출력 예시
<bos>Below is an instruction that describes a task, paired with an input that provides further context. Write a response that appropriately completes the request.

### Instruction:
Kubeflow의 Katib에 대해 설명해 주세요.

### Input:


### Response:
Kubeflow의 katib는 Kubeflow에서 제공하는 A/B 테스트 및 최적화 도구입니다.<eos>