> ## Documentation Index
> Fetch the complete documentation index at: https://docs.cloud.vessl.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Gemma 4 파인튜닝하기

> QLoRA와 Unsloth로 Gemma 4 E4B를 파인튜닝하는 전체 과정을 안내해요.

이 가이드에서는 VESSL Cloud에서 **QLoRA**와 **Unsloth**를 사용해 **Gemma 4 E4B**를 파인튜닝하는 전체 과정을 다뤄요. 가이드를 마치면 공유 스토리지에 저장된 파인튜닝 어댑터를 바로 추론이나 팀 협업에 활용할 수 있어요.

<Frame>
  <img src="https://mintcdn.com/dora/LUWzC5lx5RwOccfw/images/gemma4-hero.png?fit=max&auto=format&n=LUWzC5lx5RwOccfw&q=85&s=fc937938505425f92b7d3d8c0b735f53" alt="Google Gemma 4 공식 이미지" width="1300" height="733" data-path="images/gemma4-hero.png" />
</Frame>

## 사전 준비

시작하기 전에 다음 항목을 확인해 주세요:

* 크레딧이 충전된 VESSL Cloud 계정([가입하기](https://cloud.vessl.ai/~/signup))
* **A100 SXM 80 GB** GPU 인스턴스에 접근 가능한 조직
* Python과 Hugging Face Transformers에 대한 기본 지식

<Tip>
  VESSL Cloud가 처음이라면 [멤버 퀵스타트](/ko/guides/get-started/quickstart-user)를 먼저 완료해서 계정, 결제, 스토리지를 설정해 주세요.
</Tip>

## Workspace 만들기

<Steps>
  <Step title="스토리지 볼륨 설정하기">
    이 워크플로우에는 두 가지 스토리지가 필요해요:

    | 스토리지 타입             | 마운트 경로    | 용도                                                 |
    | ------------------- | --------- | -------------------------------------------------- |
    | **Cluster storage** | `/root`   | 홈 디렉터리. pip 패키지와 conda 환경이 Workspace 재시작 후에도 유지돼요. |
    | **Object storage**  | `/shared` | 모델 체크포인트와 결과물. 모든 클러스터에서 접근 가능하고 팀원과 공유할 수 있어요.    |

    **왜 Cluster storage를 `/root`에 마운트하나요?** 홈 디렉터리(`$HOME`)는 pip이 패키지를 설치하는 기본 경로예요. 여기에 Cluster storage를 마운트하면 `pip install`을 한 번만 실행하면 돼요. Workspace를 일시정지/재개해도 패키지가 그대로 남아있어요.

    **왜 Object storage를 `/shared`에 마운트하나요?** 파인튜닝한 모델 가중치는 다른 Workspace나 클러스터에서도 접근할 수 있어야 해요. Object storage는 S3 기반이라 어디서든 접근 가능하고, 팀원과 결과를 공유하거나 다른 리전에서 배포하기 편해요.

    Workspace를 만들기 전에 두 볼륨을 모두 생성해 주세요:

    * **Cluster storage**: 사이드바에서 **Cluster storage**를 클릭하고 **Create new volume**을 눌러주세요. 자세한 내용은 [스토리지 개요](/ko/admin/storage/overview)를 참고해 주세요.
    * **Object storage**: 사이드바에서 **Object storage**를 클릭하고 **Create new volume**을 눌러주세요. 자세한 내용은 [볼륨 만들기](/ko/member/volume/create)를 참고해 주세요.

    <Warning>
      Object storage를 `/root`에 마운트하지 마세요. Object storage는 Cluster storage보다 느려서 메인 작업공간으로 적합하지 않아요. `/shared` 같은 별도 마운트 포인트를 사용해 주세요.
    </Warning>
  </Step>

  <Step title="Workspace 실행하기">
    아래 설정으로 새 Workspace를 만들어 주세요:

    | 설정                  | 값                                             |
    | ------------------- | --------------------------------------------- |
    | **GPU**             | A100 SXM 80 GB                                |
    | **GPU 수량**          | 1                                             |
    | **이미지**             | `pytorch/pytorch:2.5.1-cuda12.4-cudnn9-devel` |
    | **Cluster storage** | Cluster storage 볼륨을 `/root`에 마운트              |
    | **Object storage**  | Object storage 볼륨을 `/shared`에 마운트             |

    전체 생성 과정은 [Workspace 만들기](/ko/member/workspace/create)를 참고해 주세요.

    <Info>
      4비트 양자화(QLoRA)를 사용하면 E4B 모델의 VRAM 사용량이 약 **18-22 GB** 수준이에요. A100의 80 GB에서 충분한 여유가 있어서 배치 사이즈를 늘리거나 시퀀스 길이를 더 길게 설정할 수도 있어요.
    </Info>
  </Step>

  <Step title="Workspace에 접속하기">
    Workspace 상태가 **Running**으로 바뀌면 JupyterLab이나 SSH로 접속해 주세요. [Workspace 접속하기](/ko/member/workspace/connect)를 참고해 주세요.
  </Step>
</Steps>

## 패키지 설치

Workspace 터미널을 열고 필요한 라이브러리를 설치해 주세요:

```bash theme={null}
pip install unsloth trl transformers datasets
```

<Note>
  Cluster storage를 `/root`에 마운트했다면 이 패키지들은 Workspace를 일시정지/재개해도 유지돼요. 한 번만 실행하면 돼요.
</Note>

## 모델 로드

```python theme={null}
from unsloth import FastModel

model, tokenizer = FastModel.from_pretrained(
    model_name="unsloth/gemma-4-E4B-it",
    max_seq_length=2048,
    load_in_4bit=True,
    full_finetuning=False,
)
```

**`load_in_4bit=True`가 하는 일:** 기본적으로 모델 파라미터는 16비트 부동소수점으로 로드되는데, 4비트 양자화는 QLoRA(NF4) 기법으로 가중치를 4비트로 압축해요. 이렇게 하면 메모리 사용량이 약 4배 줄어들어서, 원래 약 32 GB의 VRAM이 필요한 모델이 약 8-10 GB로 들어가요. 품질 저하는 미미한데, 고정된 기본 가중치만 양자화되고 LoRA 어댑터는 전체 정밀도로 학습하기 때문이에요.

<div>
  <Frame>
    <img src="https://mintcdn.com/dora/LUWzC5lx5RwOccfw/images/gemma4-vram.png?fit=max&auto=format&n=LUWzC5lx5RwOccfw&q=85&s=5cd1082788c78a6c385b717fdc3528de" alt="A100 80 GB에서 학습 중 nvidia-smi가 보고한 피크 VRAM 약 11 GB" width="1246" height="592" data-path="images/gemma4-vram.png" />
  </Frame>

  <small>이 가이드 그대로 돌렸을 때 A100 80 GB에서 피크 VRAM이 약 11 GB(13.7%)예요. 80 GB에 한참 여유가 있어서 배치 사이즈를 키우거나 시퀀스를 더 길게 잡을 수 있어요.</small>
</div>

## LoRA 어댑터 설정

```python theme={null}
model = FastModel.get_peft_model(
    model,
    finetune_vision_layers=False,
    finetune_language_layers=True,
    finetune_attention_modules=True,
    finetune_mlp_modules=True,
    r=8,
    lora_alpha=8,
    lora_dropout=0,
    bias="none",
    random_state=3407,
)
```

### 하이퍼파라미터 설명

| 파라미터                         | 값        | 설명                                                                                                                  |
| ---------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------- |
| `r`                          | `8`      | **LoRA 랭크**: 저랭크 어댑터의 용량을 결정해요. 값이 높을수록(16, 32) 복잡한 패턴을 포착하지만 메모리를 더 사용하고 소규모 데이터셋에서 과적합 위험이 있어요. 일반 태스크는 8로 시작하세요. |
| `lora_alpha`                 | `8`      | **스케일링 팩터**: 어댑터 출력이 얼마나 증폭되는지 결정해요. 보통 `r`과 같은 값으로 설정해요. 어댑터의 실질 학습률은 `lora_alpha / r`에 비례해요.                      |
| `lora_dropout`               | `0`      | **드롭아웃 비율**: 학습 중 어댑터 출력을 0으로 만들 확률이에요. 소규모 데이터셋에서는 0으로 설정하고, 대규모 데이터셋에서 과적합이 보이면 0.05-0.1로 올려보세요.                  |
| `finetune_vision_layers`     | `False`  | Gemma 4는 멀티모달 모델이에요. 텍스트만 학습할 때는 `False`로 설정해서 비전 인코더 레이어를 건너뛰세요.                                                   |
| `finetune_language_layers`   | `True`   | 언어 모델 레이어를 학습해요.                                                                                                    |
| `finetune_attention_modules` | `True`   | 어텐션(Q, K, V, O) 프로젝션 행렬을 학습해요.                                                                                      |
| `finetune_mlp_modules`       | `True`   | 피드포워드(MLP) 레이어를 학습해요. 어텐션 모듈과 함께 모델에서 가장 영향력 있는 부분을 커버해요.                                                           |
| `bias`                       | `"none"` | 바이어스 항을 학습하지 않아요. 품질 저하 없이 어댑터 크기를 작게 유지해요.                                                                         |
| `random_state`               | `3407`   | 재현성을 위한 랜덤 시드예요.                                                                                                    |

<Tip>
  **`r`을 올려야 하는 경우:**

  * **복잡한 도메인 적응** (의료, 법률, 코드): `r=16` 시도
  * **대규모 다양한 데이터셋** (10만개 이상): `r=16` 또는 `r=32` 시도
  * **단순한 스타일 변환이나 포맷 맞추기**: `r=8`이면 충분해요.
</Tip>

## 데이터셋 준비

이 예제에서는 **FineTome-100k** 데이터셋에서 3,000개 샘플을 사용해 빠른 데모를 진행해요. 프로덕션에서는 전체 데이터셋을 사용하거나 자체 데이터로 대체해 주세요.

```python theme={null}
from unsloth.chat_templates import get_chat_template, standardize_data_formats
from datasets import load_dataset

tokenizer = get_chat_template(tokenizer, chat_template="gemma-4")
dataset = load_dataset("mlabonne/FineTome-100k", split="train[:3000]")
dataset = standardize_data_formats(dataset)

def formatting_prompts_func(examples):
    convos = examples["conversations"]
    texts = [
        tokenizer.apply_chat_template(
            convo, tokenize=False, add_generation_prompt=False
        ).removeprefix("<bos>")
        for convo in convos
    ]
    return {"text": texts}

dataset = dataset.map(formatting_prompts_func, batched=True)
```

<Accordion title="직접 만든 데이터 사용하기">
  데이터셋은 각 항목에 `conversations` 필드가 있는 JSON 파일이어야 해요. `role`과 `content`가 포함된 메시지 객체의 리스트 형태예요:

  ```json theme={null}
  [
    {
      "conversations": [
        {"role": "user", "content": "프랑스의 수도는 어디인가요?"},
        {"role": "assistant", "content": "프랑스의 수도는 파리예요."}
      ]
    },
    {
      "conversations": [
        {"role": "user", "content": "광합성을 간단히 설명해 주세요."},
        {"role": "assistant", "content": "광합성은 식물이 햇빛, 물, 이산화탄소를 포도당과 산소로 변환하는 과정이에요."}
      ]
    }
  ]
  ```

  다음과 같이 로드하면 돼요:

  ```python theme={null}
  dataset = load_dataset("json", data_files="/shared/my-data.json", split="train")
  dataset = standardize_data_formats(dataset)
  dataset = dataset.map(formatting_prompts_func, batched=True)
  ```
</Accordion>

<Note>
  3,000개 샘플은 데모 목적이에요. 품질을 제대로 끌어올리려면 전체 10만 개 데이터셋이나 최소 1-2만 개의 고품질 자체 데이터를 써 주세요.
</Note>

## 학습

```python theme={null}
from trl import SFTTrainer, SFTConfig
from unsloth.chat_templates import train_on_responses_only

trainer = SFTTrainer(
    model=model,
    tokenizer=tokenizer,
    train_dataset=dataset,
    args=SFTConfig(
        dataset_text_field="text",
        per_device_train_batch_size=1,
        gradient_accumulation_steps=4,
        warmup_steps=5,
        max_steps=60,
        learning_rate=2e-4,
        logging_steps=1,
        optim="adamw_8bit",
        weight_decay=0.001,
        lr_scheduler_type="linear",
        seed=3407,
        report_to="none",
        output_dir="/shared/gemma4-finetuned",
    ),
)

trainer = train_on_responses_only(
    trainer,
    instruction_part="<|turn>user\n",
    response_part="<|turn>model\n",
)

trainer_stats = trainer.train()
```

### 학습 파라미터 설명

| 파라미터                          | 값                            | 설명                                                                                       |
| ----------------------------- | ---------------------------- | ---------------------------------------------------------------------------------------- |
| `per_device_train_batch_size` | `1`                          | GPU당 포워드 패스에 사용할 샘플 수. VRAM에 맞추기 위해 1로 설정해요.                                             |
| `gradient_accumulation_steps` | `4`                          | 4스텝마다 그래디언트를 누적한 후 가중치를 업데이트해요. **실질 배치 사이즈 = 1 x 4 = 4.**                               |
| `warmup_steps`                | `5`                          | 처음 5스텝 동안 학습률을 선형으로 올려서 초기 학습을 안정화해요.                                                    |
| `max_steps`                   | `60`                         | 총 학습 스텝 수. 데모용이에요. 전체 학습을 하려면 `max_steps`를 제거하고 `num_train_epochs=1` (또는 그 이상)로 설정해 주세요. |
| `learning_rate`               | `2e-4`                       | QLoRA 파인튜닝에 일반적으로 사용하는 학습률이에요.                                                           |
| `optim`                       | `"adamw_8bit"`               | 8비트 AdamW 옵티마이저. 양자화된 옵티마이저 상태를 사용해서 표준 AdamW 대비 약 2 GB의 VRAM을 절약해요.                     |
| `weight_decay`                | `0.001`                      | 과적합 방지를 위한 L2 정규화예요.                                                                     |
| `lr_scheduler_type`           | `"linear"`                   | 학습 과정에서 학습률을 0까지 선형으로 감소시켜요.                                                             |
| `report_to`                   | `"none"`                     | 외부 로깅(Weights & Biases 등)을 비활성화해요. 실험 추적이 필요하면 `"wandb"`로 설정하세요.                         |
| `output_dir`                  | `"/shared/gemma4-finetuned"` | Object storage에 체크포인트를 저장해서 영속성과 공유가 가능하게 해요.                                            |

**`train_on_responses_only`가 하는 일:** 기본적으로 전체 대화(사용자 + 어시스턴트 턴)에 대해 손실을 계산하는데, 이 옵션은 사용자 턴을 마스킹해서 모델이 어시스턴트 응답만 학습하게 해요. 학습 효율이 올라가고 모델이 사용자 프롬프트를 외우는 것을 방지해요.

## 평가

학습이 끝나면 모델 성능을 정성적, 정량적으로 확인해 보세요.

### 학습 손실 확인

`trainer_stats` 객체에 학습 로그가 담겨 있어요. 손실 곡선이 감소하면 모델이 학습하고 있다는 뜻이에요:

```python theme={null}
import json

# 최종 학습 손실 출력
print(f"Final training loss: {trainer_stats.training_loss:.4f}")

# 로그 히스토리에서 스텝별 손실 확인
for entry in trainer_stats.log_history[-5:]:
    if "loss" in entry:
        print(f"  Step {entry['step']}: loss = {entry['loss']:.4f}")
```

<Tip>
  정상적인 손실 곡선은 높은 값(2-3 이상)에서 시작해 꾸준히 감소해요. 너무 일찍 정체되면 `r`을 올리거나 데이터를 더 사용해 보세요. 손실이 급등하거나 발산하면 학습률을 낮춰보세요.
</Tip>

<div>
  <Frame>
    <img src="https://mintcdn.com/dora/LUWzC5lx5RwOccfw/images/gemma4-training-loss.png?fit=max&auto=format&n=LUWzC5lx5RwOccfw&q=85&s=d6c8cf28192d44a8715642b60db3b2b7" alt="60 step 학습에서 손실이 약 2.37에서 안정적으로 감소하는 곡선" width="2000" height="1010" data-path="images/gemma4-training-loss.png" />
  </Frame>

  <small>실제 60 step 학습 손실 로그예요. 2.37 근처에서 시작해 꾸준히 감소하는 전형적인 SFT 곡선이에요.</small>
</div>

### 학습 전후 비교

파인튜닝된 모델로 동일한 프롬프트를 실행해서 효과를 확인해 보세요:

```python theme={null}
from transformers import TextStreamer

messages = [
    {"role": "user", "content": [
        {"type": "text", "text": "Explain quantum computing in simple terms."}
    ]}
]

_ = model.generate(
    **tokenizer.apply_chat_template(
        messages,
        add_generation_prompt=True,
        tokenize=True,
        return_dict=True,
        return_tensors="pt",
    ).to("cuda"),
    max_new_tokens=256,
    use_cache=True,
    temperature=0.7,
    streamer=TextStreamer(tokenizer, skip_prompt=True),
)
```

<Accordion title="홀드아웃 테스트 분할로 평가하기">
  더 엄밀한 평가를 위해 데이터를 미리 분할하고 홀드아웃 부분의 손실을 계산할 수 있어요:

  ```python theme={null}
  # 학습 전에 데이터셋 분할
  split = dataset.train_test_split(test_size=0.1, seed=3407)
  train_dataset = split["train"]
  eval_dataset = split["test"]

  # SFTTrainer에 eval_dataset 전달
  trainer = SFTTrainer(
      model=model,
      tokenizer=tokenizer,
      train_dataset=train_dataset,
      eval_dataset=eval_dataset,
      args=SFTConfig(
          # ... 위와 동일한 설정 + 추가:
          eval_strategy="steps",
          eval_steps=20,
          # ...
      ),
  )
  ```

  이렇게 하면 일정 간격으로 평가 손실을 보고해서, 과적합(학습 손실은 감소하는데 평가 손실은 증가)을 감지할 수 있어요.
</Accordion>

## 모델 저장

파인튜닝된 어댑터와 토크나이저를 Object storage에 저장해요:

```python theme={null}
model.save_pretrained("/shared/gemma4-finetuned/final")
tokenizer.save_pretrained("/shared/gemma4-finetuned/final")
```

`/shared`가 Object storage에 마운트되어 있으므로 저장된 모델은:

* **영속적**: Workspace를 종료해도 유지돼요.
* **크로스 클러스터**: 어느 리전의 Workspace에서든 접근할 수 있어요.
* **팀 공유 가능**: 해당 볼륨에 접근 가능한 팀원 누구나 어댑터를 로드할 수 있어요.

<div>
  <Frame>
    <img src="https://mintcdn.com/dora/LUWzC5lx5RwOccfw/images/gemma4-saved-model.png?fit=max&auto=format&n=LUWzC5lx5RwOccfw&q=85&s=b3851698276505c98dd64c84b4a59686" alt="학습이 끝난 뒤 /shared/gemma4-finetuned/final/ 디렉터리에 저장된 어댑터와 토크나이저 파일 목록" width="1148" height="1426" data-path="images/gemma4-saved-model.png" />
  </Frame>

  <small>학습이 끝난 뒤 `/shared/gemma4-finetuned/final/`에 저장된 어댑터·토크나이저 파일이에요. Object storage라서 다른 클러스터나 팀원의 Workspace에서도 그대로 마운트해 읽을 수 있어요.</small>
</div>

다른 Workspace에서 어댑터를 로드하려면:

```python theme={null}
from unsloth import FastModel

model, tokenizer = FastModel.from_pretrained(
    model_name="unsloth/gemma-4-E4B-it",
    max_seq_length=2048,
    load_in_4bit=True,
    full_finetuning=False,
)

from peft import PeftModel
model = PeftModel.from_pretrained(model, "/shared/gemma4-finetuned/final")
```

## Gemma 4 모델 비교

GPU와 태스크 복잡도에 맞는 Gemma 4 변형을 선택해 주세요:

| 모델              | 파라미터 | 권장 GPU                  | QLoRA VRAM 추정치 | 적합한 용도                 |
| --------------- | ---- | ----------------------- | -------------- | ---------------------- |
| **Gemma 4 E2B** | 2B   | T4 16 GB, L4 24 GB      | 약 6-8 GB       | 빠른 실험, 엣지 배포           |
| **Gemma 4 E4B** | 4B   | A100 40/80 GB, L4 24 GB | 약 10-14 GB     | 품질과 효율의 최적 균형          |
| **Gemma 4 12B** | 12B  | A100 80 GB              | 약 18-24 GB     | 더 높은 품질, 단일 GPU 파인튜닝   |
| **Gemma 4 27B** | 27B  | A100 80 GB(빠듯함), 2xA100 | 약 32-40 GB     | 프론티어급 품질, 더 많은 VRAM 필요 |

<Info>
  VRAM 추정치는 QLoRA 4비트 양자화, 배치 사이즈 1, 시퀀스 길이 2048 기준이에요. 실제 사용량은 배치 사이즈, 시퀀스 길이, 그래디언트 누적 설정에 따라 달라져요.
</Info>

## 다음 단계

* **실행 가능한 레시피 받기**: 이 워크플로우를 패키징한 버전(인터랙티브 노트북 + `vesslctl` 배치 잡 경로, 소규모 도메인 QA 데이터셋으로 학습)이 [gemma4-finetuning 쿡북 레시피](https://github.com/vessl-ai/vessl-cloud-cookbook/tree/main/gemma4-finetuning)에 있어요. 실측 비용과 VRAM 벤치마크도 함께 들어 있어요.
* **자체 데이터 활용**: FineTome-100k를 도메인 특화 대화 데이터로 바꿔서 모델을 내 용도에 맞춰 보세요.
* **DPO 또는 ORPO 적용**: SFT 이후 선호도 최적화(DPO/ORPO)를 적용해서 모델을 원하는 방향으로 더 정렬할 수 있어요.
* **모델 키우기**: 더 높은 품질이 필요하면 12B 또는 27B 모델로 올려 보세요. 대규모 데이터셋에는 `r=16` 또는 `r=32`를 사용하세요.
* **GGUF 내보내기**: 파인튜닝된 모델을 GGUF 포맷으로 변환해서 llama.cpp나 Ollama로 로컬 추론을 할 수 있어요.
* **배치 잡으로 실행**: [VESSL Cloud 배치 잡](/ko/member/job/overview)을 사용해서 파인튜닝을 스케줄링 가능한 재현 가능 파이프라인으로 실행해 보세요.
* **vesslctl로 자동화**: [`vesslctl job create`](/ko/cli/quickstart) 한 줄로 이 실험을 재현 가능한 배치 잡으로 제출할 수 있어요. Jupyter 셀 대신 `train.py`에 담고, 터미널이나 CI 파이프라인에서 바로 돌려보세요.
