13년차의 서버실

서버, 인프라, 홈랩 기술 블로그

[태그:] LoRA

  • [AI] Ollama 커스텀 LLM 가이드: Modelfile 사용법과 실전 구축

    [AI] Ollama 커스텀 LLM 가이드: Modelfile 사용법과 실전 구축

    Ollama 커스텀 LLM 가이드: Modelfile 사용법과 실전 구축

    Ollama 커스텀 LLM을 오래 만지다 보면, 결국 성능 숫자보다 출력 습관을 통제하고 싶다는 요구가 더 자주 생기더라고요. 답변은 꼭 한국어로 나오게 하고 싶다든지, 명령어를 먼저 보여주고 설명은 뒤로 보내고 싶다든지, 모르면 지어내지 말고 확인이 필요하다고 말하게 만들고 싶을 때가 그렇습니다. 이럴 때 가장 먼저 손대기 좋은 게 Modelfile입니다.

    다만 기대치는 정확히 잡아야 합니다. Modelfile은 보통 사람들이 떠올리는 “모델 재학습” 자체와는 다릅니다. 실무 감각으로 말하면, 베이스 모델의 가중치를 새로 만드는 도구라기보다 실행 규약을 모델 단위로 고정하는 계층에 가깝습니다. 이 차이를 이해해 두면 왜 어떤 문제는 SYSTEM 한 줄로 풀리고, 어떤 문제는 ADAPTER나 RAG까지 가야 하는지 훨씬 빨리 감이 옵니다.

    이번 글은 로컬 AI 모델 개발 관점에서, Ollama의 Modelfile로 어디까지 바꿀 수 있는지, 어디서 멈추는 게 맞는지, 그리고 실제로 자주 깨지는 지점이 무엇인지까지 한 번에 정리한 글입니다. 문법과 API 필드는 Ollama 공식 Modelfile 문서, Create API 문서, Generate API 문서 기준으로 다시 확인했습니다.

    로컬 환경에서 베이스 모델 위에 Modelfile을 얹어 커스텀 모델을 만드는 전체 흐름입니다.

    1. Ollama 커스텀 LLM이 실무에서 먹히는 이유는 재현성 때문입니다

    제가 Modelfile을 계속 쓰는 이유는 단순합니다. 사람은 매번 같은 프롬프트를 정교하게 붙이기 어렵지만, 파일은 늘 같은 방식으로 동작하거든요. 특히 팀 단위로 로컬 모델을 쓰기 시작하면 모델 품질보다 먼저 문제 되는 게 사람마다 다른 사용 습관입니다. 어떤 분은 시스템 프롬프트를 길게 넣고, 어떤 분은 짧게 넣고, 어떤 분은 아예 안 넣습니다.

    이때 Modelfile은 계약서를 하나 만들어 줍니다. SYSTEM, TEMPLATE, PARAMETER, MESSAGE, ADAPTER를 모델 이름 뒤에 고정해 두니, 같은 이름을 호출하는 한 최소한의 행동 일관성은 확보됩니다. 이거 진짜 편하더라고요. 문서 초안 작성, 운영 가이드 보조, 코드 리뷰 보조처럼 반복성이 높은 작업일수록 차이가 분명합니다.

    • 반복 프롬프트 제거: 매번 붙이던 역할 설명과 금지 규칙을 모델 단위로 고정합니다.
    • 출력 형식 표준화: “명령어 먼저, 설명 나중” 같은 우선순위를 팀 공통 규약으로 만들기 좋습니다.
    • 실험 버전 분리: 같은 베이스 모델에서 보수형, 설명형, 요약형 모델을 나눠 비교하기 쉽습니다.
    • 장애 원인 분리: 모델 자체 문제인지, 템플릿 문제인지, 파라미터 문제인지 추적이 빨라집니다.

    2. Ollama 커스텀 LLM에서 먼저 선을 그어야 합니다: Modelfile이 바꾸는 것과 못 바꾸는 것

    여기서 선을 분명히 그어야 삽질이 줄어듭니다. Modelfile은 행동 규칙, 입력 형식, 실행 파라미터, 예시 대화, 어댑터 연결은 잘 다룹니다. 반면 모델 내부 지식 자체를 새로 학습시키는 일은 기본적으로 하지 못합니다. 문서 몇 줄 넣었다고 도메인 지식을 완전히 새로 습득하는 구조는 아니라는 뜻입니다.

    실제로는 이렇게 나누면 편합니다. “답변의 순서나 톤이 문제냐”면 Modelfile 쪽입니다. “모델이 특정 도메인 지식을 계속 모르거나 아는 척 틀리느냐”면 ADAPTER, RAG, 또는 더 맞는 베이스 모델을 검토하는 편이 맞습니다.

    문제 유형 우선 선택 이유 피해야 할 접근
    답변 톤이 들쭉날쭉함 SYSTEM + MESSAGE 행동 규칙과 말투는 프롬프트 계층에서 통제가 잘 됩니다 바로 ADAPTER부터 붙이기
    출력 형식이 자꾸 깨짐 TEMPLATE + stop 재검토 채팅 포맷 불일치일 가능성이 큽니다 모델 성능 탓으로 돌리기
    응답이 너무 산만하거나 창의성이 과함 PARAMETER 조정 temperature, top_p, num_ctx 영향이 큽니다 SYSTEM 문구만 계속 길게 늘리기
    특정 도메인 지식을 안정적으로 못 씀 ADAPTER 또는 RAG 검토 행동 지시만으로는 한계가 분명합니다 MESSAGE 예시를 과도하게 누적하기
    팀원마다 결과가 다름 Modelfile로 모델 이름 표준화 입력 규약 자체를 버전 관리할 수 있습니다 사람마다 프롬프트 템플릿 수동 복붙

    3. Modelfile 핵심 지시어는 많아 보여도 실무 우선순위는 분명합니다

    공식 문서에 나오는 지시어는 여러 개지만, 처음 모델을 잡을 때 자주 쓰는 순서는 거의 고정입니다. 보통 FROM → SYSTEM → PARAMETER → MESSAGE가 1차 세트입니다. TEMPLATE와 ADAPTER는 필요성이 분명할 때만 올리는 편이 안전합니다.

    지시어 실무 우선순위 주로 쓰는 상황 주의점
    FROM 최상 출발 베이스 모델 지정 이 선택이 나머지 품질과 호환성의 기준점이 됩니다
    SYSTEM 최상 역할, 금지사항, 출력 순서 고정 길이보다 우선순위가 중요합니다
    PARAMETER 높음 일관성, 컨텍스트, 중단 토큰 제어 값 하나로 안정성이 흔들릴 수 있습니다
    MESSAGE 높음 few-shot 예시 내장 예시를 많이 넣을수록 답변이 경직되기 쉽습니다
    TEMPLATE 중간 채팅 포맷을 명시적으로 제어할 때 베이스 템플릿과 stop이 어긋나면 토큰 누수가 납니다
    ADAPTER 상황 의존 LoRA 또는 QLoRA 결과물 적용 베이스 모델 불일치가 나면 품질이 급격히 무너집니다
    LICENSE 공유 시 중요 배포 또는 팀 공유 내부 사용과 외부 배포 조건을 분리해서 봐야 합니다
    REQUIRES 환경 의존 특정 Ollama 최소 버전 고정 버전 미충족이면 원인 모를 생성 실패처럼 보일 수 있습니다

    효율이 좋은 규칙은 하나입니다. 첫 번째 성공 모델은 단순하게 만들고, 두 번째 버전부터 정교화하는 쪽이 낫습니다. 처음부터 요소를 다 얹으면 실패했을 때 어느 레이어가 원인인지 식별하기가 어렵습니다.

    4. 실전 구현 1단계: 베이스 모델의 원본 템플릿부터 확인하세요

    많이 놓치는 단계인데, 사실상 필수에 가깝습니다. 커스텀 모델을 만들기 전에 ollama show --modelfile로 베이스 모델의 기본 구조를 먼저 봐야 합니다. 이유는 간단합니다. 모델마다 기대하는 채팅 템플릿과 stop 토큰이 다를 수 있기 때문입니다.

    ollama pull llama3.2
    ollama show --modelfile llama3.2
    ollama show --modelfile llama3.2 > base-llama3.2.Modelfile

    저는 보통 세 번째 줄까지 같이 해둡니다. 원본을 파일로 떠놓으면 수정 중에 기준점을 잃지 않거든요. 특히 stop 토큰과 TEMPLATE 블록은 기억으로 다시 쓰기보다 원본에서 필요한 만큼만 복사하는 쪽이 훨씬 안전합니다.

    그다음 1차 버전은 작게 갑니다. 출력 우선순위, 언어 정책, 추측 금지 같은 운영 규약만 넣어도 충분한 경우가 많습니다.

    mkdir -p ~/ollama-custom/workshop
    cd ~/ollama-custom/workshop
    cat > Modelfile <<'EOF'
    FROM llama3.2
    PARAMETER temperature 0.2
    PARAMETER num_ctx 4096
    SYSTEM """당신은 한국어로 답변하는 인프라 엔지니어 보조 AI입니다.
    답변은 항상 다음 순서를 지킵니다.
    1. 바로 실행할 명령어나 설정 예시
    2. 명령어가 실패할 때 확인할 지점
    3. 마지막에 짧은 설명
    모르는 내용은 추측하지 말고 확인이 필요하다고 말합니다."""
    MESSAGE user 리눅스 디스크 사용량 확인 방법 알려줘
    MESSAGE assistant 먼저 아래 명령어부터 확인하겠습니다.
    df -h
    sudo du -sh /var/* 2>/dev/null | sort -h
    lsblk
    EOF
    
    ollama create infra-assistant -f Modelfile
    ollama run infra-assistant

    포인트는 SYSTEM을 길게 쓰는 게 아니라 답변 순서와 금지 규칙을 관찰 가능한 형태로 명시하는 것입니다. “친절하게 답변해라” 같은 추상 문구보다 “명령어 먼저, 설명은 나중” 같은 규칙이 훨씬 잘 먹는 편입니다.

    실제 작업 흐름은 터미널에서 Modelfile을 편집하고 곧바로 create, run으로 검증하는 식으로 진행됩니다.

    5. PARAMETER는 품질보다도 행동 습관을 바꾸는 손잡이입니다

    파라미터를 만질 때 흔히 정답률만 보게 되는데, 실무에선 그보다 출력의 흔들림이 줄어드는가를 먼저 보는 게 맞습니다. 로컬 모델을 업무 보조로 쓸 때는 늘 비슷한 형식으로 나오는 답이 훨씬 값질 때가 많습니다. 이 부분은 실제 운영해 보면 체감이 꽤 큽니다.

    자주 보는 건 temperature, num_ctx, stop, 그리고 필요할 때 샘플링 계열 파라미터입니다. 여기서 특히 실수하기 쉬운 건 num_ctx입니다. 컨텍스트를 크게 잡으면 좋아 보이지만 메모리 사용량과 프롬프트 평가 시간도 같이 늘어납니다.

    파라미터 낮게 둘 때 높게 둘 때 판단 기준
    temperature 일관성 높음, 답변이 보수적 표현 다양성 증가, 흔들림도 증가 운영 문서/명령어 보조면 낮게, 아이디어 발산이면 높게
    num_ctx 메모리 부담 감소, 빠름 긴 문맥 유지에 유리, 느려질 수 있음 평소 넣는 입력 길이를 기준으로 최소 충분치만 확보
    stop 잘 맞으면 출력 경계가 깔끔함 불필요하거나 틀리면 출력이 잘리거나 토큰 누수 베이스 모델 원본 값에서 출발
    MESSAGE 예시 수 유연성 유지 답변 스타일 고정력 상승, 과하면 경직 대개 1~3개로도 충분

    실무적으로는 이렇게 나누면 편합니다.

    • 문서 초안, 운영 절차, 명령어 추천: temperature를 낮게 두고 형식 일관성을 우선합니다.
    • 브레인스토밍, 문안 변주: temperature를 조금 올리되, SYSTEM에서 출력 구조는 유지합니다.
    • 긴 로그/설정 해석: num_ctx를 무작정 키우기보다 먼저 입력을 잘라 넣을 수 있는지 확인합니다.

    6. TEMPLATE는 마지막에 건드리세요. 건드릴 땐 stop까지 세트로 보셔야 합니다

    TEMPLATE는 멋있어 보이지만 실제로는 가장 쉽게 사고 나는 영역입니다. 추천하는 원칙은 단순합니다. 기본 템플릿으로 원하는 형식이 나오면 TEMPLATE는 건드리지 않는 것입니다. SYSTEM과 MESSAGE만으로 해결되는 경우가 훨씬 많습니다.

    반대로 TEMPLATE를 건드려야 하는 경우도 있습니다. 베이스 모델의 채팅 포맷을 명시적으로 유지하면서 시스템, 사용자, 응답 경계를 통제하고 싶을 때입니다. 이때 핵심은 예쁘게 다시 쓰는 게 아니라 원본 구조를 최대한 보존하면서 필요한 부분만 조정하는 겁니다.

    FROM llama3.2
    TEMPLATE """{{ if .System }}<|start_header_id|>system<|end_header_id|>
    
    {{ .System }}<|eot_id|>{{ end }}{{ if .Prompt }}<|start_header_id|>user<|end_header_id|>
    
    {{ .Prompt }}<|eot_id|>{{ end }}<|start_header_id|>assistant<|end_header_id|>
    
    {{ .Response }}<|eot_id|>"""
    PARAMETER stop "<|start_header_id|>"
    PARAMETER stop "<|end_header_id|>"
    PARAMETER stop "<|eot_id|>"
    SYSTEM """당신은 한국어 운영 문서를 절차형으로 정리하는 도우미입니다."""

    이 블록에서 꼭 봐야 할 건 둘입니다.

    • {{ .System }}, {{ .Prompt }}, {{ .Response }}가 실제 템플릿에 반영되는지
    • TEMPLATE에 등장하는 특수 토큰과 PARAMETER stop이 서로 맞물리는지

    실패 사례도 꽤 단순합니다. 템플릿은 바꿨는데 stop은 예전 값을 그대로 두는 경우입니다. 그러면 응답 본문에 <|eot_id|> 같은 문자열이 그대로 보이거나, 반대로 답변이 중간에서 잘립니다. 이건 모델이 멍청해서가 아니라 출력 종료 경계가 틀어진 것입니다.

    7. ADAPTER는 지식 보강보다 정렬된 편향 추가에 가깝게 보는 편이 맞습니다

    ADAPTER를 붙이면 개인화 폭이 넓어지긴 합니다. 다만 기대를 너무 크게 잡으면 실망도 큽니다. 먼저 물어볼 건 하나입니다. “이 문제를 SYSTEM과 MESSAGE로 해결할 수 없는가?” 여기서 해결되면 굳이 어댑터를 안 붙이는 편이 운영은 더 쉽습니다.

    어댑터가 필요한 상황은 보통 두 가지입니다. 첫째, 특정 도메인 문체나 응답 습관을 훨씬 강하게 고정해야 할 때입니다. 둘째, 베이스 모델만으로는 일관되게 안 나오는 패턴을 추가 학습 결과로 밀어 넣고 싶을 때입니다.

    FROM llama3.2
    ADAPTER ./adapters/my-lora-adapter.gguf
    SYSTEM """당신은 Kubernetes 운영 가이드를 작성하는 보조 모델입니다.
    항상 점검 순서, 명령어, 장애 추정 원인을 순서대로 제시합니다."""

    여기서 제일 중요한 건 어댑터가 학습된 베이스 모델과 FROM의 베이스 모델이 맞아야 한다는 점입니다. 공식 문서도 이 경우 결과가 erratic해질 수 있다고 안내합니다. 현장에서는 이걸 모델 성능 탓으로 오해하는 경우가 적지 않습니다.

    그래서 저는 순서를 이렇게 잡습니다.

    1. ADAPTER 없이 동작하는 1차 모델을 만듭니다.
    2. 동일 질문 세트로 결과를 저장합니다.
    3. 그다음 ADAPTER를 붙인 버전을 따로 만듭니다.
    4. 스타일 개선인지, 지식 개선인지, 아니면 품질 붕괴인지 비교합니다.
    Ollama 커스텀 LLM에 LoRA 어댑터를 적용하는 구조 이미지

    베이스 모델 위에 LoRA 어댑터를 추가해 도메인 특화 성격을 강화하는 흐름입니다.

    8. API 자동화는 실험이 많아질수록 가치가 커집니다

    한두 개 만들 땐 Modelfile이 읽기 편합니다. 그런데 역할별 모델이 늘어나면 CLI 수작업은 금방 귀찮아집니다. 이때는 POST /api/create를 써서 생성 과정을 코드로 고정하는 편이 낫습니다. 모델 이름 규칙, 파라미터 조합, 예시 대화 구성을 반복해서 재현해야 할 때 특히 강합니다.

    curl http://localhost:11434/api/create -d '{
      "model": "infra-helper-api",
      "from": "llama3.2",
      "system": "You are a Korean infrastructure engineering assistant. Provide commands first, then failure checks, then short explanations.",
      "parameters": {
        "temperature": 0.2,
        "num_ctx": 4096
      },
      "messages": [
        {
          "role": "user",
          "content": "nginx 로그 확인 순서 알려줘"
        },
        {
          "role": "assistant",
          "content": "먼저 access.log와 error.log를 분리해서 보고, 그다음 4xx/5xx 비율과 upstream 오류를 확인하겠습니다."
        }
      ]
    }'

    이 방식을 선호하는 이유는 단순히 편해서만은 아닙니다. 실험 조건을 텍스트로 남길 수 있기 때문입니다. 어떤 모델 이름이 어떤 규칙과 파라미터로 만들어졌는지가 남아야 나중에 비교가 됩니다.

    생성 후에는 성능 체감만 보지 말고 API 응답 지표도 같이 보는 편이 좋습니다. Ollama의 생성 응답에는 load_duration, prompt_eval_count, prompt_eval_duration, eval_count, eval_duration 같은 필드가 포함됩니다. 이걸 보면 느린 이유가 로딩인지, 입력 길이인지, 출력 토큰 수인지 분리해서 보기 훨씬 편합니다.

    curl http://localhost:11434/api/generate -d '{
      "model": "infra-assistant",
      "prompt": "systemd 서비스 상태 확인 절차를 알려줘",
      "stream": false
    }'

    여기서 판단 포인트는 이렇습니다.

    • load_duration이 크면 모델 로딩 비용이 큰 겁니다.
    • prompt_eval_duration이 크면 입력 문맥이 길거나 num_ctx 운용이 과한 경우를 의심할 수 있습니다.
    • eval_duration이 크면 출력 토큰 수가 많거나 실행 자원이 부족한 쪽일 가능성이 큽니다.

    9. 트러블슈팅은 증상보다 근본 원인을 붙잡아야 빨리 끝납니다

    실제로 많이 부딪히는 문제는 대부분 네 부류였습니다. 중요한 건 증상을 외우는 게 아니라 어느 레이어가 깨졌는지를 먼저 가르는 겁니다. SYSTEM 문제인지, TEMPLATE 문제인지, ADAPTER 문제인지, 자원 문제인지 구분만 잘해도 시간이 꽤 절약됩니다.

    1) 시스템 프롬프트가 먹지 않는 것처럼 보일 때

    • 먼저 볼 것: ollama show --modelfile 모델명
    • 근본 원인: TEMPLATE 안에 {{ .System }} 반영이 없거나 베이스 템플릿을 수정하면서 시스템 경계를 깨뜨린 경우가 많습니다.
    • 판단 기준: 말투보다도 “추측 금지”, “명령어 우선” 같은 규칙이 반복적으로 무시되는지 봅니다.
    • 해결 방향: 원본 TEMPLATE로 되돌린 뒤 SYSTEM만 남기고 다시 비교합니다.

    2) 답변에 특수 토큰이 섞일 때

    • 먼저 볼 것: TEMPLATE와 PARAMETER stop 조합
    • 근본 원인: 출력 종료 토큰과 템플릿 토큰 경계가 맞지 않는 상황입니다.
    • 판단 기준: <|eot_id|>, <|start_header_id|> 같은 문자열이 본문에 보이면 거의 이쪽입니다.
    • 해결 방향: 베이스 모델 기본 Modelfile의 stop 설정을 그대로 기준 삼아 다시 맞춥니다.

    3) 어댑터 적용 후 품질이 갑자기 무너질 때

    • 먼저 볼 것: ADAPTER 학습 기준 모델
    • 근본 원인: FROM에 적은 베이스 모델과 어댑터가 기대하는 기반 모델 불일치
    • 판단 기준: 문장 붕괴, 주제 일탈, 불필요한 반복이 적용 직후 심해집니다.
    • 해결 방향: 어댑터 제작 시 사용한 기반 계열과 동일한 모델로 FROM을 맞춥니다.

    4) 속도가 너무 느리거나 메모리가 버거울 때

    • 먼저 볼 것: ollama ps, OS 메모리/스왑 사용량, 입력 길이, num_ctx
    • 근본 원인: 모델 크기보다도 과도한 컨텍스트, 동시 실행 모델 수, 자원 부족이 겹치는 경우가 많습니다.
    • 판단 기준: 스왑이 발생하면 체감 성능은 급격히 떨어집니다.
    • 해결 방향: 작은 베이스 모델로 내리거나 num_ctx를 낮추고 실험 모델 동시 구동 수를 줄입니다.

    저는 장애를 볼 때 항상 이렇게 나눕니다. 형식 이상은 TEMPLATE, 성격 이상은 SYSTEM/MESSAGE, 지식 이상은 ADAPTER 또는 베이스 모델, 속도 이상은 자원과 컨텍스트. 이 프레임으로 보면 원인 추적 속도가 빨라집니다.

    Ollama 커스텀 LLM 트러블슈팅과 검증 과정을 보여주는 이미지

    응답 토큰 이상 출력, 시스템 프롬프트 무시, 어댑터 불일치 같은 대표 장애 포인트를 점검하는 장면입니다.

    10. 검증은 더 똑똑해졌는가보다 의도대로 수렴하는가를 보셔야 합니다

    커스텀 모델을 만들고 나면 자꾸 성능 향상만 보게 됩니다. 그런데 Modelfile의 1차 목적은 대개 정답률 폭증이 아니라 출력 습관 고정입니다. 그래서 검증 질문 세트를 별도로 두고 베이스 모델과 커스텀 모델을 같은 질문으로 비교하는 편이 좋습니다.

    ollama run llama3.2 "systemd 서비스 상태 확인 절차를 알려줘"
    ollama run infra-assistant "systemd 서비스 상태 확인 절차를 알려줘"
    ollama run infra-assistant "모르면 추측하지 말고, nginx 502 점검 순서를 알려줘"

    여기서 보는 건 단순히 답변 길이나 그럴듯함이 아닙니다.

    1. 명령어가 먼저 나오는지
    2. 설명이 뒤로 밀리는지
    3. 모를 때 지어내지 않는지
    4. 한국어 톤과 형식이 유지되는지
    5. MESSAGE 예시가 답변을 과하게 복제하게 만들지는 않는지
    검증 항목 베이스 모델 커스텀 모델 합격 기준
    답변 언어 영문/국문 혼합 가능 국문 고정 가능 언어 정책이 흔들리지 않을 것
    명령어 우선 제시 질문마다 다름 일관되게 맞출 수 있음 초반 2~3줄 안에 바로 실행 항목이 나올 것
    톤 일관성 변동 가능 SYSTEM으로 안정화 질문이 바뀌어도 문체가 크게 흔들리지 않을 것
    도메인 집중도 일반 지식 중심 MESSAGE/ADAPTER로 강화 쓸데없는 일반론보다 절차형 답변이 앞설 것
    추측 억제 상황 따라 흔들림 금지 규칙 반영 가능 불확실할 때 확인 필요성을 분명히 말할 것

    현업 자동화에서는 “베이스 모델보다 더 똑똑하다”보다 “늘 같은 포맷으로 나온다”가 더 중요할 때가 많습니다. 운영 문서 초안, 반복 질의 응답, 내부 가이드 작성은 특히 그렇습니다. 이 지점이 바로 Ollama 커스텀 LLM의 실무 가치라고 봐도 무리가 없습니다.

    11. FAQ와 최종 추천: Ollama 커스텀 LLM은 이럴 땐 A, 저럴 땐 B로 나누면 편합니다

    자주 헷갈리는 질문

    • Q. Modelfile만 쓰면 미세 조정이 된 건가요?
      A. 아닙니다. 기본적으로는 설정, 프롬프트 구조, 예시 대화, 실행 파라미터를 묶는 작업에 가깝습니다. 추가 학습 결과를 붙이려면 ADAPTER 같은 별도 레이어가 필요합니다.
    • Q. TEMPLATE는 꼭 수정해야 하나요?
      A. 아닙니다. 기본 템플릿으로 문제가 없으면 안 건드리는 쪽이 맞습니다. 깨지는 빈도가 높은 영역이라 이유가 분명할 때만 손대는 게 좋습니다.
    • Q. 개인화된 AI를 가장 빨리 만드는 방법은?
      A. 베이스 모델 하나를 고르고, SYSTEM에 역할과 출력 순서를 명시한 뒤, MESSAGE 1~2개만 넣어 1차 버전을 만드는 방식이 가장 빠릅니다.
    • Q. 언제 ADAPTER까지 가야 하나요?
      A. SYSTEM과 MESSAGE로도 해결되지 않는 도메인 특화 패턴이 반복해서 필요할 때입니다. 단순한 말투 고정 정도라면 대개 과한 선택입니다.

    선택 기준은 이렇게 정리하면 편합니다.

    • 빠르게 목적형 모델이 필요하다: FROM + SYSTEM + PARAMETER + MESSAGE로 시작하세요. 가장 안전하고 재현성이 좋습니다.
    • 응답 형식을 강하게 통제해야 한다: 먼저 ollama show --modelfile로 원본을 확인한 뒤 TEMPLATE를 최소 수정하세요.
    • 특정 학습 결과물을 이미 갖고 있다: ADAPTER를 붙이되, 베이스 모델 호환성을 제일 먼저 확인하세요.
    • 역할별 모델을 반복 생성해야 한다: 사람이 수동으로 만들기보다 /api/create로 자동화하는 편이 관리가 낫습니다.
    • 속도와 메모리가 아슬아슬하다: 큰 모델 집착보다 작은 모델 + 낮은 num_ctx + 명확한 SYSTEM이 더 실용적일 때가 많습니다.

    한 줄로 압축하면 이렇습니다. Ollama 커스텀 LLM의 핵심은 모델을 새로 발명하는 게 아니라, 내 작업 방식을 반복 가능하게 고정하는 것입니다. 처음부터 무거운 학습 파이프라인으로 들어가기보다, Modelfile로 행동 규약을 먼저 굳히는 쪽이 훨씬 현실적입니다. 관련해서 로컬 LLM 성능 최적화나 RAG 비교 글도 함께 읽어보시면 다음 단계 판단이 더 쉬워집니다.

    Ollama 커스텀 LLM Modelfile 핵심 요소 요약 인포그래픽

    FROM, SYSTEM, PARAMETER, TEMPLATE, ADAPTER를 언제 선택하면 되는지 한눈에 정리한 요약 이미지입니다.

  • [AI] Stable Diffusion 고급 활용: 이미지 일관성 유지 및 워크플로우 최적화 팁

    [AI] Stable Diffusion 고급 활용: 이미지 일관성 유지 및 워크플로우 최적화 팁

    Stable Diffusion 고급 활용: 이미지 일관성 유지 및 워크플로우 최적화 팁

    안녕하세요, 13년차 서버실 지킴이입니다. 오늘은 제가 홈랩에서 Stable Diffusion (스테이블 디퓨전)을 가지고 놀면서 겪었던 삽질과 그 해결 과정을 공유해 볼까 합니다. 요즘 AI 이미지 생성, 정말 신기하고 재미있잖아요? 텍스트 몇 줄만 입력하면 기가 막힌 이미지가 뚝딱 나오고요. 그런데 말입니다, 막상 특정 캐릭터나 일관된 스타일을 유지하면서 여러 장의 이미지를 뽑으려고 하면 생각보다 쉽지 않더라고요. 😅

    “프롬프트 (Prompt, 명령문) 조금만 바꿔도 전혀 다른 인물이 나와요!” “이거 작업하려면 너무 오래 걸리고, 원하는 결과 얻기가 복불복이에요!” 혹시 이런 경험 있으신가요? 제가 처음 Stable Diffusion 고급 활용에 도전했을 때 딱 그랬습니다. 원하는 그림을 뽑아내기 위해 수십 번, 수백 번 돌려봐야 했고, 그러다 보면 VRAM (Video Random Access Memory, 그래픽 카드 메모리) 부족으로 고통받기도 했죠. ㅋㅋㅋ

    그래서 오늘은 이런 문제들을 해결하고, AI 이미지 일관성을 유지하면서 Stable Diffusion 워크플로우를 최적화할 수 있는 몇 가지 팁과 노하우를 제 경험을 바탕으로 솔직하게 알려드리려고 합니다. 삽질하며 배운 소중한 경험들이니, 독자분들께도 큰 도움이 될 거라고 확신합니다! 자, 그럼 시작해 볼까요? 🎉

    Stable Diffusion 고급 워크플로우 개요 다이어그램

    Stable Diffusion 고급 워크플로우 개요를 나타내는 다이어그램입니다.

    개념 설명: 왜 일관성 유지가 어려울까요?

    Stable Diffusion은 기본적으로 텍스트 프롬프트를 기반으로 노이즈 (Noise, 무작위 신호)에서 이미지를 생성하는 확률적 모델이에요. 쉽게 말해, 매번 새로운 이미지를 생성할 때마다 주사위를 던지는 것과 같다고 볼 수 있어요. 그래서 같은 프롬프트를 넣어도 미묘하게 다른 결과가 나오는 거죠. 이게 바로 AI 이미지 일관성 유지가 어려운 근본적인 이유입니다.

    하지만 걱정 마세요! 이 문제를 해결하기 위한 강력한 도구들이 있어요. 핵심 개념들을 먼저 살펴보고 갈게요. 제가 처음에 이게 뭔가 싶었는데, 결국 이런 원리더라고요.

    • Seed (시드): 이미지 생성의 초기 노이즈 패턴을 결정하는 고유한 숫자 값이에요. 같은 시드와 프롬프트를 사용하면 아주 유사한 이미지를 얻을 수 있어요. 일관성 유지의 기본 중의 기본입니다.

    • LoRA (Low-Rank Adaptation, 로라): 특정 스타일, 캐릭터, 또는 개념을 학습시킨 경량 모델이에요. 기존 Stable Diffusion 모델에 추가하여 특정 특징을 강조하거나 반영할 때 사용합니다. 적은 용량으로도 강력한 커스터마이징이 가능하죠.

    • ControlNet (컨트롤넷): 이미지의 포즈 (Pose), 깊이 (Depth), 선 (Canny)과 같은 구조적 정보를 정밀하게 제어할 수 있게 해주는 확장 기능이에요. 특정 자세나 구도를 유지하면서 이미지를 생성할 때 정말 유용해요.

    • IP-Adapter (Image Prompt Adapter, 아이피 어댑터): 프롬프트 입력 없이 참조 이미지 (Reference Image)의 스타일이나 내용을 반영하여 이미지를 생성하는 기술이에요. 기존 이미지의 색감, 분위기, 심지어 특정 특징까지 새로운 이미지에 손쉽게 전이시킬 수 있어요. 이거 진짜 물건이더라고요! 💡

    실전 구현: 일관성 유지를 위한 핵심 워크플로우

    자, 그럼 이제 이 강력한 도구들을 어떻게 활용해서 Stable Diffusion 워크플로우를 최적화하고 AI 이미지 일관성을 유지할 수 있는지 제가 직접 해본 경험을 바탕으로 알려드리겠습니다. 제가 직접 해보니 이 조합이 제일 좋더라고요!

    1. Seed 값 고정 및 Variation Seed 활용

    가장 기본적인 단계지만, 가장 중요해요. 특정 이미지가 마음에 들었다면, 그 이미지의 Seed 값을 반드시 확인하고 고정해야 합니다. 그래야 다음 생성 시에도 일관된 베이스를 유지할 수 있거든요.

    하지만 똑같은 이미지만 뽑아낼 수는 없잖아요? 여기서 Variation Seed (변형 시드)를 활용하면 좋아요. 원본 시드의 일관성을 유지하면서 미묘한 변화를 줄 수 있죠. 저는 보통 Variation Strength (변형 강도)를 0.1~0.2 정도로 낮게 설정해서 사용해요.

    # Stable Diffusion WebUI (Automatic1111) 기준
    # txt2img 탭에서 Seed 값에 원하는 숫자 입력
    # Variation Seed 활성화 후 Seed 값 입력 (또는 -1로 랜덤)
    # Variation Strength를 0.1 ~ 0.2 정도로 설정
    

    2. LoRA를 통한 캐릭터/스타일 학습 및 적용

    특정 캐릭터나 화풍을 일관되게 유지하고 싶다면 LoRA는 필수예요. Civitai (시비타이) 같은 커뮤니티에서 잘 만들어진 LoRA를 다운로드받아 사용하는 것도 좋지만, 정말 나만의 캐릭터를 만들고 싶다면 직접 LoRA를 학습시키는 것도 좋은 방법입니다. (나중에 기회가 되면 나만의 LoRA 학습에 대해서도 다뤄볼게요!)

    프롬프트에 LoRA를 적용하는 방법은 간단해요. 보통 <code><lora:LoRAName:weight> 형식으로 사용하죠. weight 값으로 LoRA의 적용 강도를 조절할 수 있어요.

    # 프롬프트 예시
    beautiful girl, in a park, <lora:my_character_v1:0.7>, wearing a white dress
    

    3. ControlNet으로 포즈와 구도 제어

    같은 캐릭터가 다양한 포즈를 취하는 이미지를 만들 때 ControlNet만큼 강력한 도구는 없어요. 저는 주로 OpenPose (오픈포즈)를 사용해서 원하는 포즈를 스켈레톤 (Skeleton) 형태로 입력하고, Canny (캐니)나 Depth (뎁스)를 사용해서 이미지의 윤곽선이나 깊이 정보를 제어합니다. 이게 진짜 편하더라고요!

    예를 들어, 웹에서 찾은 특정 인물의 포즈 이미지를 ControlNet의 OpenPose Preprocessor (전처리자)에 넣으면, 그 포즈를 그대로 따라 하는 이미지를 생성할 수 있어요. 여러 개의 ControlNet을 동시에 사용할 수도 있는데, 이 경우 가중치 (Weight) 조절이 중요해요. ⚠️

    Stable Diffusion ControlNet 설정 화면 예시

    Stable Diffusion WebUI에서 ControlNet을 설정하는 화면 예시입니다.

    4. IP-Adapter로 레퍼런스 이미지 스타일 전이

    이건 제가 정말 좋아하는 기능인데요! 프롬프트로 색감이나 분위기를 설명하는 것이 어려울 때, IP-Adapter는 정말 빛을 발해요. 특정 레퍼런스 이미지의 색상 팔레트 (Color Palette)나 전체적인 분위기를 새로운 이미지에 입히고 싶을 때 사용하면 좋아요.

    저는 주로 캐릭터의 룩앤필 (Look and Feel)을 유지하면서 배경이나 의상을 바꿀 때 IP-Adapter를 활용합니다. 참조 이미지를 업로드하고 적절한 Weight를 조절하면, 프롬프트만으로는 얻기 힘든 결과물을 쉽게 얻을 수 있어요. 이거 한 번 써보시면 헤어 나오기 어려울 겁니다. 😉

    Stable Diffusion IP-Adapter 설정 화면 예시

    Stable Diffusion WebUI에서 IP-Adapter를 설정하는 화면 예시입니다.

    주의사항 및 트러블슈팅: 삽질 피하기! ⚠️

    제가 13년 동안 인프라 삽질을 하면서 느낀 건, 어떤 기술이든 만능은 없다는 거예요. Stable Diffusion 고급 활용도 마찬가지에요. 제가 겪었던 몇 가지 문제와 해결법을 공유합니다.

    • LoRA와 ControlNet의 과도한 사용: 너무 많은 LoRA나 ControlNet을 동시에 사용하면 이미지 퀄리티가 떨어지거나, 아예 엉뚱한 결과가 나올 수 있어요. 각 기능의 Weight를 낮게 시작해서 점진적으로 올리면서 최적 값을 찾아야 합니다. 저도 처음엔 이게 뭔가 싶었는데, 결국 하나씩 빼가면서 테스트해봤죠.

    • Seed Variation의 과한 강도: Variation Strength를 너무 높게 설정하면 원본 시드의 일관성이 깨져버려요. 일관성 유지가 목적이라면 0.1~0.3 사이의 낮은 값을 권장합니다.

    • VRAM 부족 문제: ControlNet을 여러 개 사용하거나, 높은 해상도의 이미지를 생성하거나, 배치 사이즈 (Batch Size)를 늘리면 VRAM이 순식간에 동납니다. 아이고, VRAM이 터지더라고요! ㅋㅋㅋ

      • 해결책: Stable Diffusion 실행 시 --xformers, --lowvram 또는 --medvram 옵션을 사용해보세요. xformers는 메모리 사용량을 줄여주면서 속도도 향상시켜줘요. 저는 이걸로 한숨 돌렸습니다.
    • 프롬프트의 섬세한 조정: 아무리 강력한 도구를 써도 결국 프롬프트가 모호하거나 일관성이 없으면 좋은 결과를 얻기 어려워요. 핵심 키워드는 명확하게, 부정 프롬프트 (Negative Prompt)는 꼼꼼하게 작성하는 습관을 들이는 것이 중요합니다.

    검증 및 결과: 실제 적용 사례

    위에 설명드린 Seed 고정, LoRA, ControlNet, IP-Adapter 조합을 활용해서 제가 직접 캐릭터 시리즈를 만들어봤습니다. 결과는 대만족이었어요! 🎉

    이전에는 프롬프트 조금만 바꿔도 다른 사람이 나왔었는데, 이제는 정말 특정 캐릭터의 얼굴 특징, 의상, 심지어 분위기까지 일관되게 유지하면서 다양한 포즈와 배경의 이미지를 생성할 수 있게 됐어요. 드디어 됐다! 이 맛에 삽질하는 거 아니겠습니까? ㅎㅎ

    예를 들어, 제가 만든 LoRA 캐릭터 모델을 기반으로, ControlNet으로 특정 운동 포즈를 잡아주고, IP-Adapter로 빈티지한 사진의 색감을 입혔더니, 마치 한 작가가 그린 듯한 일관된 시리즈 이미지를 얻을 수 있었어요. 이 과정에서 Stable Diffusion 워크플로우는 훨씬 효율적으로 바뀌었고요. AI 아트 최적화의 진정한 의미를 깨달은 순간이었죠.

    Stable Diffusion 고급 기법 적용 전후 이미지 일관성 유지 결과 비교

    Stable Diffusion 고급 기법을 적용했을 때와 적용하지 않았을 때의 이미지 일관성 유지 결과를 비교하는 예시입니다.

    마무리: 13년차의 제언

    오늘은 Stable Diffusion 고급 활용을 통해 AI 이미지 일관성을 유지하고 워크플로우를 최적화하는 방법에 대해 제가 겪었던 경험과 팁들을 공유해드렸어요. Seed, LoRA, ControlNet, 그리고 IP-Adapter를 잘 조합하면 여러분도 원하는 결과물을 훨씬 쉽고 효율적으로 얻을 수 있을 겁니다.

    결국 인프라 엔지니어로서 AI 이미지 생성도 시스템 최적화의 연장선이더라고요. 어떤 도구를 어떻게 조합하고, 어떤 변수를 조절하느냐에 따라 결과가 천차만별이니까요.

    이 글이 여러분의 Stable Diffusion 여정에 작은 등불이 되었으면 좋겠어요. 다음 글에서는 ComfyUI (컴피유아이) 같은 노드 기반의 고급 워크플로우 자동화 툴이나, 나만의 LoRA 학습 방법에 대해서도 다뤄볼 예정이니 기대해주세요! 혹시 이런 경험 있으신가요? 댓글로 자유롭게 공유해주세요! 😊

  • [AI] LLM 파인튜닝 실전 가이드: LoRA, QLoRA로 커스텀 모델 만들기

    [AI] LLM 파인튜닝 실전 가이드: LoRA, QLoRA로 커스텀 모델 만들기

    LLM 파인튜닝 실전 가이드: LoRA, QLoRA로 커스텀 모델 만들기

    안녕하세요, 13년차 인프라 엔지니어 ‘서버실’입니다. 요즘 LLM(Large Language Model)의 세상이 정말 빠르게 변하고 있죠? ChatGPT 같은 거대 모델들을 보면서, ‘아, 나도 우리 회사 데이터로, 혹은 나만의 특화된 지식으로 LLM을 만들 수 없을까?’ 하는 생각, 혹시 해보셨나요?

    사실 저도 이런 꿈을 꾸다가 거대한 모델을 통째로 학습시키려면 엄청난 GPU 자원이 필요하다는 현실의 벽에 부딪혔어요. 엔비디아(NVIDIA) H100 같은 최신 GPU는 가격도 만만치 않고요. 게다가 학습 시간도 정말 어마어마하더라고요. 홈랩에서 저만의 서버를 돌리는 저 같은 사람에게는 엄두도 못 낼 일이었습니다. 😅

    하지만 희망은 있더라고요! 바로 PEFT(Parameter-Efficient Fine-Tuning, 파라미터 효율적 파인튜닝) 기법 덕분입니다. 특히 LoRA(Low-Rank Adaptation)와 이를 더 발전시킨 QLoRA(Quantized LoRA)는 제한된 자원으로도 LLM 파인튜닝을 가능하게 해주는 정말 혁신적인 방법이거든요.

    오늘은 제가 직접 여러 모델을 파인튜닝하며 겪었던 삽질 경험과 함께, LoRA와 QLoRA를 활용해서 나만의 커스텀 LLM을 만드는 실전 가이드를 여러분께 공유해드릴게요. 자, 그럼 함께 시작해볼까요?

    LLM 파인튜닝의 핵심, LoRA와 QLoRA의 기본 원리를 한눈에 볼 수 있는 다이어그램입니다.

    핵심 개념 설명: PEFT, LoRA, QLoRA 이해하기

    본격적인 실전에 앞서, 핵심 개념들을 짚고 넘어가는 게 중요해요. 제가 처음엔 이 용어들이 좀 헷갈렸거든요.

    • LLM (Large Language Model, 거대 언어 모델): 방대한 텍스트 데이터를 학습하여 사람의 언어를 이해하고 생성하는 능력을 가진 AI 모델입니다. 예를 들면 GPT-3, Llama 2 같은 모델들이죠.
    • Fine-tuning (파인튜닝): 이미 학습된 거대 모델(Pre-trained Model)을 특정 작업이나 데이터셋에 맞게 추가로 학습시키는 과정입니다. 예를 들어, 의료 질문 답변에 특화된 모델을 만들고 싶다면, 의료 데이터로 파인튜닝하는 식이죠.
    • PEFT (Parameter-Efficient Fine-Tuning, 파라미터 효율적 파인튜닝): 이 녀석이 바로 우리의 구세주입니다! 기존의 파인튜닝은 모델 전체의 모든 파라미터를 업데이트해야 해서 엄청난 자원이 필요했어요. 하지만 PEFT는 모델의 아주 작은 부분(소수의 파라미터)만 학습시키거나, 기존 모델에 작은 모듈을 추가해서 학습 효율을 극대화하는 기법들을 총칭합니다. 덕분에 적은 GPU 메모리로도 파인튜닝이 가능해지는 거죠.
    • LoRA (Low-Rank Adaptation, 저랭크 적응): PEFT의 대표적인 방법 중 하나에요. 쉽게 말해, 거대한 LLM의 가중치(weights)를 직접 수정하는 대신, 원본 가중치 옆에 아주 작은 두 개의 행렬(low-rank matrices)을 추가해서 학습시키는 방식입니다. 이 작은 행렬들만 학습시키고, 원본 모델의 가중치는 그대로 두는 거죠. 이렇게 하면 학습해야 할 파라미터 수가 극적으로 줄어들고, 학습된 어댑터(adapter)의 크기도 매우 작아서 저장 및 로드도 훨씬 수월해요.
    • QLoRA (Quantized LoRA, 양자화된 LoRA): LoRA를 한 단계 더 발전시킨 기법이에요. 기존 LoRA는 원본 모델의 가중치를 16비트 부동소수점(FP16)으로 불러와야 했는데, QLoRA는 이를 4비트 정수(4-bit quantization)로 양자화(quantization)해서 불러옵니다. 이렇게 되면 모델을 로드하는 데 필요한 GPU 메모리가 획기적으로 줄어들어요. 제가 홈랩에서 GPU 메모리가 부족해서 겪었던 수많은 삽질 끝에 찾은 빛과 같은 존재였습니다! 덕분에 8GB나 12GB GPU로도 7B(70억 개 파라미터) 이상의 LLM을 파인튜닝할 수 있게 된 거죠.

    LLM 파인튜닝 실전 구현: LoRA, QLoRA 단계별 적용

    자, 이제 이론은 충분해요! 제가 직접 홈랩에서 시도했던 과정을 바탕으로, LoRA와 QLoRA를 활용한 모델 학습 실전 가이드를 단계별로 알려드릴게요.

    1. 환경 준비

    먼저 필요한 라이브러리들을 설치해야 합니다. 파이썬(Python) 환경은 기본이겠죠? 저는 주로 가상 환경(Virtual Environment)을 만들어서 작업하는데, 깔끔하더라고요.

    # 가상 환경 생성 및 활성화
    python -m venv llm_finetune_env
    source llm_finetune_env/bin/activate
    
    # 필요한 라이브러리 설치
    # transformers: Hugging Face 모델 및 트레이너
    # peft: 파라미터 효율적 파인튜닝 (LoRA, QLoRA 등)
    # bitsandbytes: 4비트 양자화 지원
    # accelerate: 분산 학습 가속화
    # datasets: 데이터셋 로드 및 처리
    # trl: 트랜스포머 강화 학습 (SFTTrainer 사용)
    pip install transformers peft bitsandbytes accelerate datasets trl torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
    

    여기서 <code>torch 설치 시 CUDA 버전에 맞는 URL을 사용해야 해요. 저는 CUDA 12.1 환경이라 cu121을 썼지만, 여러분의 환경에 맞춰 조절해주세요. ⚠️ GPU 드라이버와 CUDA Toolkit 버전이 제대로 맞지 않으면 모델 학습 시 에러가 발생할 수 있으니 꼭 확인하셔야 합니다!

    2. 데이터셋 준비

    파인튜닝에 사용할 데이터셋을 준비해야 합니다. 저는 간단한 질문-답변 형식의 JSON 파일을 사용했어요. 실제 프로젝트에서는 여러분의 목적에 맞는 데이터를 잘 정제하는 것이 무엇보다 중요합니다.

    # example_dataset.json
    [
      {
        "instruction": "다음 질문에 대해 간결하게 답변해 주세요.",
        "input": "LLM 파인튜닝이 무엇인가요?",
        "output": "LLM 파인튜닝은 이미 학습된 거대 언어 모델을 특정 작업이나 데이터셋에 맞게 추가로 학습시키는 과정입니다."
      },
      {
        "instruction": "LoRA의 장점을 설명해 주세요.",
        "input": "",
        "output": "LoRA는 원본 모델의 가중치를 고정하고 작은 행렬만 학습하여, 파라미터 수를 획기적으로 줄이고 학습 효율을 높이는 파인튜닝 기법입니다."
      }
    ]
    

    이 데이터를 파이썬에서 불러와서 datasets 라이브러리로 처리하면 돼요.

    3. 모델 로드 및 양자화 설정 (QLoRA)

    이제 Hugging Face에서 적당한 LLM 모델을 불러옵니다. 저는 Llama 2 7B 모델을 예시로 들어볼게요. QLoRA를 사용하려면 bitsandbytes 설정을 잘 해줘야 합니다.

    import torch
    from transformers import AutoTokenizer, AutoModelForCausalLM, BitsAndBytesConfig
    
    # 모델 ID (예시: 메타의 Llama-2 7B 모델)
    model_id = "meta-llama/Llama-2-7b-hf" # 실제 사용 시 접근 권한 필요
    
    # 4비트 양자화 설정
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True, # 4비트 양자화로 로드
        bnb_4bit_use_double_quant=True, # 이중 양자화 사용
        bnb_4bit_quant_type="nf4", # NormalFloat 4 (NF4) 양자화 타입
        bnb_4bit_compute_dtype=torch.bfloat16 # 계산 시 사용할 데이터 타입
    )
    
    # 토크나이저 및 모델 로드
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    model = AutoModelForCausalLM.from_pretrained(
        model_id,
        quantization_config=bnb_config,
        device_map="auto" # 여러 GPU가 있다면 자동으로 분배
    )
    model.config.use_cache = False # 학습 중 캐시 비활성화
    model.config.pretraining_tp = 1 # 사전 학습 텐서 병렬 처리 설정
    

    여기서 model_id는 실제 접근 가능한 모델 ID로 변경하셔야 해요. Llama 2 같은 모델은 Hugging Face에서 접근 권한을 요청해야 하거든요. 저는 보통 공개된 모델 중 하나를 선택해서 실험합니다.

    4. LoRA 설정 및 모델 학습 준비

    peft 라이브러리를 사용해서 LoRAConfig를 정의합니다. 여기서 r (LoRA 랭크)와 lora_alpha 값이 정말 중요해요.

    from peft import LoraConfig, get_peft_model, prepare_model_for_kbit_training
    
    # 4비트 학습을 위해 모델 준비
    model = prepare_model_for_kbit_training(model)
    
    # LoRA 설정
    lora_config = LoraConfig(
        r=16, # LoRA 랭크. 값이 클수록 표현력이 좋지만 파라미터도 늘어남.
        lora_alpha=32, # LoRA 스케일링 계수. r의 두 배 정도가 일반적.
        target_modules=["q_proj", "k_proj", "v_proj", "o_proj", "gate_proj", "up_proj", "down_proj"], # LoRA를 적용할 모듈
        bias="none", # 편향(bias) 학습 여부. 'none'이 일반적.
        lora_dropout=0.05, # LoRA 레이어에 적용할 드롭아웃
        task_type="CAUSAL_LM", # 작업 유형 (인과 언어 모델)
    )
    
    # PEFT 모델 생성
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters() # 학습 가능한 파라미터 수 확인
    

    target_modules는 LoRA를 적용할 트랜스포머 블록 내의 레이어들을 지정해요. 주로 쿼리(query), 키(key), 밸류(value), 아웃풋(output) 프로젝션 레이어에 적용하죠.

    LoRA 설정 후 학습 가능한 파라미터 수가 얼마나 줄었는지 확인하는 모습입니다. 정말 극적으로 줄어들죠?

    5. 트레이너 설정 및 모델 학습

    이제 trl 라이브러리의 SFTTrainer를 사용해서 모델 학습을 시작합니다. SFTTrainer는 지도 파인튜닝(Supervised Fine-Tuning)에 특화되어 있어서 데이터셋 처리가 정말 편해요.

    from trl import SFTTrainer
    from transformers import TrainingArguments
    from datasets import load_dataset
    
    # 데이터셋 로드 (위에서 만든 example_dataset.json 사용)
    dataset = load_dataset("json", data_files="example_dataset.json")
    
    # 훈련 인자 설정
    training_args = TrainingArguments(
        output_dir="./results", # 결과 저장 디렉토리
        num_train_epochs=3, # 학습 에포크 수
        per_device_train_batch_size=2, # GPU당 배치 크기 (메모리 제약 시 줄임)
        gradient_accumulation_steps=4, # 기울기 누적 스텝 수 (가상 배치 크기 증가)
        optim="paged_adamw_8bit", # 8비트 AdamW 옵티마이저 (메모리 효율적)
        learning_rate=2e-4, # 학습률
        logging_steps=10, # 로깅 스텝
        save_strategy="epoch", # 에포크마다 모델 저장
        evaluation_strategy="no", # 평가 전략 (간단 예시에서는 평가 생략)
        fp16=False, # QLoRA 사용 시 FP16은 비활성화
        bf16=True, # bfloat16 사용 (A100, RTX 30/40 시리즈 등 지원 GPU)
    )
    
    # SFTTrainer 생성
    trainer = SFTTrainer(
        model=model,
        train_dataset=dataset["train"],
        peft_config=lora_config,
        dataset_text_field="input", # 텍스트 필드 지정 (데이터셋 구조에 따라 다름)
        max_seq_length=512, # 최대 시퀀스 길이
        tokenizer=tokenizer,
        args=training_args,
    )
    
    # 학습 시작!
    trainer.train()
    
    # 학습된 어댑터 저장
    trainer.model.save_pretrained("./my_llm_adapter")
    tokenizer.save_pretrained("./my_llm_adapter")
    

    gradient_accumulation_steps는 메모리가 부족할 때 배치 크기를 효과적으로 늘리는 방법이에요. per_device_train_batch_size * gradient_accumulation_steps가 실제 효과적인 배치 크기가 됩니다. bf16=True는 bfloat16을 지원하는 GPU에서 모델 학습 속도를 높이고 메모리 효율을 좋게 만듭니다.

    ⚠️ 주의사항 및 트러블슈팅 (삽질 경험 공유)

    제가 이 과정에서 정말 많이 겪었던 문제들이 몇 가지 있어요. 독자 여러분은 저처럼 삽질하지 마시라고 공유해드립니다!

    • ⚠️ CUDA Out of Memory (OOM) 에러: 가장 흔하게 만나는 문제일 거예요. 특히 GPU 메모리가 8GB, 12GB 정도라면 QLoRA를 써도 OOM이 발생할 수 있어요.
      • 해결책: per_device_train_batch_size를 1로 줄이고, gradient_accumulation_steps를 늘려보세요. max_seq_length도 줄이는 것이 도움이 될 수 있습니다. bitsandbytes의 bnb_4bit_compute_dtype을 torch.bfloat16 대신 torch.float16으로 바꿔보는 것도 방법입니다. (단, bf16=True 대신 fp16=True로 설정해야 합니다.)
    • ⚠️ 모델 학습 속도 저하: QLoRA는 메모리 효율적이지만, 4비트 양자화된 가중치를 매번 역양자화(dequantize)해서 계산해야 하므로 학습 속도가 약간 느려질 수 있어요.
      • 해결책: bf16=True를 사용하면 (지원 GPU 한정) 속도 개선에 도움이 됩니다. 데이터 로딩 파이프라인 최적화(예: num_workers 설정)도 고려해보세요.
    • ⚠️ 데이터셋 포맷 오류: SFTTrainer의 dataset_text_field나 formatting_func 설정이 데이터셋 구조와 맞지 않으면 에러가 납니다.
      • 해결책: 데이터셋 JSON 파일의 키(key)와 dataset_text_field가 정확히 일치하는지 확인해야 해요. 만약 복잡한 포맷이라면 formatting_func를 직접 구현해서 데이터를 원하는 형태로 만들어줘야 합니다. 저도 여기서 한참 헤맸거든요.
    • ⚠️ 커스텀 모델의 성능 부족: 아무리 파인튜닝을 해도 원하는 결과가 나오지 않을 때가 있어요.
      • 해결책: 가장 큰 원인은 데이터셋의 품질과 양입니다. 충분히 다양하고 고품질의 데이터가 아니라면 모델은 제대로 학습되지 않아요. LoRA 하이퍼파라미터(r, lora_alpha, learning_rate)를 튜닝해보는 것도 중요합니다. 저는 wandb(Weights & Biases) 같은 툴을 써서 실험 결과를 추적하며 모델 학습의 최적 파라미터를 찾곤 합니다.

    파인튜닝 모델 검증 및 결과 확인 🎉

    자, 이제 학습이 끝났으니 우리가 만든 커스텀 LLM이 얼마나 잘 작동하는지 확인해볼 차례예요!

    1. 학습된 모델 로드

    학습된 LoRA 어댑터와 토크나이저를 다시 불러옵니다. 이때 원본 모델도 함께 로드해야 해요.

    from transformers import AutoTokenizer, AutoModelForCausalLM
    from peft import PeftModel, PeftConfig
    import torch
    
    # 원본 모델 로드 (QLoRA 설정과 동일하게)
    model_id = "meta-llama/Llama-2-7b-hf"
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,
        bnb_4bit_use_double_quant=True,
        bnb_4bit_quant_type="nf4",
        bnb_4bit_compute_dtype=torch.bfloat16
    )
    base_model = AutoModelForCausalLM.from_pretrained(
        model_id,
        quantization_config=bnb_config,
        device_map="auto"
    )
    tokenizer = AutoTokenizer.from_pretrained(model_id)
    
    # 학습된 LoRA 어댑터 로드
    peft_model_id = "./my_llm_adapter"
    model = PeftModel.from_pretrained(base_model, peft_model_id)
    
    # 모델을 평가 모드로 전환 (Dropout 등 비활성화)
    model.eval()
    

    2. 추론 (Inference)

    이제 우리의 커스텀 모델에 질문을 던져봅시다. 학습 데이터셋에 있던 내용과 비슷한 질문을 던져보면, 훨씬 더 정확하고 원하는 형식의 답변을 생성하는 것을 볼 수 있을 거예요.

    # 질문 생성 (데이터셋 형식과 유사하게)
    prompt = "### Instruction:\n다음 질문에 대해 간결하게 답변해 주세요.\n\n### Input:\nLLM 파인튜닝은 무엇인가요?\n\n### Output:"
    
    # 토크나이저로 인코딩
    inputs = tokenizer(prompt, return_tensors="pt").to("cuda")
    
    # 모델로 답변 생성
    with torch.no_grad():
        outputs = model.generate(
            **inputs,
            max_new_tokens=200, # 최대 생성 토큰 수
            do_sample=True, # 샘플링 기반 생성
            top_p=0.9, # 상위 p 확률 내에서 샘플링
            temperature=0.7, # 창의성 조절
        )
    
    # 결과 디코딩 및 출력
    response = tokenizer.decode(outputs[0], skip_special_tokens=True)
    print(response)
    

    출력된 답변을 보면, 우리가 파인튜닝한 의도대로 간결하고 정확한 정보가 나오는 것을 확인할 수 있어요. 처음엔 정말 너무 기특해서 박수까지 쳤다니까요! 🎉

    파인튜닝된 LLM이 질문에 대해 답변하는 모습입니다. 우리가 의도한 대로 잘 대답하고 있죠?

    마무리 💡

    오늘은 13년차 인프라 엔지니어인 제가 직접 경험하며 익힌 LLM 파인튜닝의 세계, 특히 LoRA와 QLoRA 기법을 활용한 커스텀 LLM 구축 방법에 대해 자세히 알아봤습니다.

    이 방법들을 통해 우리는 다음과 같은 장점을 얻을 수 있더라고요.

    • ✅ GPU 메모리 효율성: QLoRA 덕분에 적은 GPU 자원으로도 거대 모델을 파인튜닝할 수 있게 되었습니다. 저처럼 홈랩을 운영하는 분들에게는 정말 큰 축복이에요!
    • ✅ 빠른 모델 학습: 전체 모델을 학습하는 대신 소수의 파라미터만 학습하여 시간을 절약할 수 있습니다.
    • ✅ 저장 및 배포 용이성: 학습된 LoRA 어댑터는 크기가 매우 작아서 관리하고 공유하기가 훨씬 편해요.

    물론, 이 과정에서 수많은 삽질과 시행착오가 있었지만, 결국 원하는 결과를 얻었을 때의 뿌듯함은 정말 대단했습니다. 여러분도 저의 경험이 시행착오를 줄이는 데 도움이 되기를 바라요.

    이제 여러분은 나만의 특화된 LLM을 만들 수 있는 강력한 도구를 손에 넣으신 거예요. 다음 단계로는 더 다양한 데이터셋으로 실험해보거나, Mistral, Gemma 같은 다른 오픈소스 LLM에 적용해보는 것도 정말 좋은 도전이 될 거예요. 궁금한 점이나 추가로 다루었으면 하는 주제가 있다면 언제든지 댓글로 남겨주세요! 다음 글에서는 이렇게 파인튜닝된 모델을 실제 서비스에 배포하는 과정에 대해 다뤄볼까 합니다. 기대해주세요!

    LoRA와 QLoRA의 주요 장점과 고려 사항을 요약한 인포그래픽입니다.

  • [AI] LLM 파인튜닝 실전 가이드: LoRA/QLoRA로 도메인 특화 모델 만들기

    “GPT한테 물어봤더니 우리 회사 용어를 하나도 모르더라고요”

    이런 경험 한 번쯤 있으시죠? 저도 작년에 사내 기술 문서 Q&A 봇을 만들려고 ChatGPT API를 붙여봤는데… 일반적인 질문엔 잘 대답하는데 우리 팀 특유의 약어나 내부 시스템 이름을 물어보면 완전 엉뚱한 소리를 하더라고요. RAG(Retrieval-Augmented Generation, 검색 기반 생성)로 어느 정도 해결은 됐는데, 근본적으로 모델 자체가 도메인을 이해하게 하고 싶다는 생각이 들었습니다.

    그래서 시작한 게 LLM 파인튜닝이었는데요. 처음엔 “GPU 수십 장 없이 가능하겠어?” 싶었는데, LoRA(Low-Rank Adaptation)와 QLoRA(Quantized LoRA) 덕분에 홈랩 서버 한 대로도 충분히 가능하다는 걸 알게 됐습니다. 오늘은 제가 직접 삽질하면서 익힌 실전 LLM 파인튜닝 과정을 공유해 드릴게요.

    ▲ LLM 파인튜닝의 전체 파이프라인 — 데이터 준비부터 학습, 추론까지의 흐름을 한눈에 볼 수 있습니다.

    LoRA / QLoRA가 뭔지부터 짚고 넘어가죠

    파인튜닝이라는 개념 자체는 간단합니다. 이미 잘 학습된 모델을 내 데이터로 추가 학습시키는 거예요. 근데 문제가 있습니다. LLaMA 3나 Mistral 같은 모델은 파라미터가 수십억 개거든요. 이걸 전부 다시 학습시키려면 A100 GPU가 여러 장 필요하고, 메모리도 수백 GB가 필요합니다. 현실적으로 홈랩에서 불가능한 수준이죠.

    그래서 나온 게 LoRA(Low-Rank Adaptation, 저랭크 적응 학습)입니다. 쉽게 말해서, 모델 전체를 학습시키는 게 아니라 “변화량”만 학습시키는 방식이에요. 원래 모델의 가중치(weight)는 그대로 얼려두고(freeze), 그 옆에 훨씬 작은 보조 행렬(adapter)만 학습시키는 거거든요.

    그리고 QLoRA(Quantized LoRA)는 여기서 한 발 더 나아가서, 원본 모델을 4비트(4-bit)로 양자화(quantization)해서 메모리를 훨씬 적게 쓰면서 LoRA 학습을 하는 방식입니다. 제가 RTX 3090 한 장으로 13B 모델을 파인튜닝할 수 있었던 게 바로 QLoRA 덕분이었어요.

    방식 전체 파인튜닝 (Full Fine-tuning) LoRA QLoRA
    학습 파라미터 수 전체 (수십억) 소수 (수백만) 소수 (수백만)
    메모리 요구량 매우 높음 중간 낮음
    원본 모델 정밀도 FP16/BF16 FP16/BF16 4-bit (NF4)
    홈랩 가능 여부 ❌ (7B 이상 어려움) △ (7B 정도 가능) ✅ (13B~34B도 가능)
    성능 손실 없음 (기준) 미미 약간 있음

    저처럼 GPU 한 장으로 실험하시는 분이라면 QLoRA가 현실적인 선택입니다. 성능 차이가 생각보다 크지 않아서 실제 서비스에서도 충분히 쓸 수 있는 수준이더라고요.

    환경 준비 — 이 부분에서 삽질 좀 했습니다 ㅎㅎ

    제 홈랩 환경 기준으로 설명드릴게요. CUDA 버전 호환성 때문에 처음에 꽤 고생했거든요. CUDA 버전과 PyTorch 버전, bitsandbytes 버전이 삼각형처럼 맞물려야 합니다. 하나라도 틀리면 에러 폭탄이 터져요.

    # Python 가상환경 먼저 만들어주세요 (conda 추천)
    conda create -n llm-finetune python=3.10
    conda activate llm-finetune
    
    # PyTorch 설치 (CUDA 11.8 기준 — 본인 환경에 맞게 조정)
    pip install torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu118
    
    # 핵심 라이브러리 설치
    pip install transformers==4.40.0
    pip install peft==0.10.0          # LoRA 구현체
    pip install bitsandbytes==0.43.0  # 4-bit 양자화
    pip install accelerate==0.29.0
    pip install datasets==2.18.0
    pip install trl==0.8.6            # SFT Trainer 포함
    
    # 설치 확인
    python -c "import torch; print(torch.cuda.is_available())"

    💡 팁: bitsandbytes는 Windows에서 설치가 까다롭습니다. WSL2나 Linux 환경을 강력 추천드려요. 저도 처음에 Windows에서 삽질하다가 결국 Ubuntu로 갈아탔습니다.

    학습 데이터 준비 — 사실 이게 제일 중요해요

    파인튜닝에서 코드보다 더 중요한 게 데이터입니다. 진짜로요. 좋은 데이터 1000개가 나쁜 데이터 10만 개보다 낫다는 걸 직접 경험했거든요.

    LLM 파인튜닝용 데이터는 보통 Instruction 형식으로 만듭니다. 이렇게 생겼어요:

    [
      {
        "instruction": "우리 회사의 배포 프로세스를 설명해줘",
        "input": "",
        "output": "우리 회사의 배포 프로세스는 다음과 같습니다. 먼저 개발자가 feature 브랜치에서 작업 후 PR을 올리면..."
      },
      {
        "instruction": "다음 에러 로그를 분석해줘",
        "input": "ERROR: Connection refused to internal-db-01:5432",
        "output": "이 에러는 PostgreSQL 데이터베이스 서버에 연결이 거부된 것입니다. 주요 원인으로는..."
      }
    ]

    이걸 학습 형식으로 변환하는 코드를 보여드릴게요:

    from datasets import Dataset
    import json
    
    def format_instruction(sample):
        """Alpaca 스타일의 프롬프트 포맷"""
        if sample["input"]:
            prompt = f"""### Instruction:
    {sample["instruction"]}
    
    ### Input:
    {sample["input"]}
    
    ### Response:
    {sample["output"]}"""
        else:
            prompt = f"""### Instruction:
    {sample["instruction"]}
    
    ### Response:
    {sample["output"]}"""
        return {"text": prompt}
    
    # 데이터 로드 및 변환
    with open("my_domain_data.json", "r", encoding="utf-8") as f:
        raw_data = json.load(f)
    
    dataset = Dataset.from_list(raw_data)
    dataset = dataset.map(format_instruction)
    print(f"총 학습 데이터: {len(dataset)}개")
    print(dataset[0]["text"][:200])  # 첫 번째 샘플 미리보기

    ⚠️ 주의사항: 데이터 품질 체크는 필수입니다. 중복 데이터, 너무 짧은 응답(10자 이하), 특수문자가 깨진 데이터는 학습 전에 꼭 걸러내세요. 저는 이걸 건너뛰었다가 모델이 이상한 패턴을 학습해서 처음부터 다시 한 적 있습니다.

    QLoRA 파인튜닝 실전 코드

    드디어 본론입니다. 제가 실제로 사용하는 학습 스크립트예요. 주석을 충분히 달아놨으니 따라가 보세요.

    ▲ QLoRA 학습 진행 중 GPU 메모리 사용량과 학습 손실(loss) 변화 — RTX 3090 기준으로 13B 모델도 충분히 학습 가능합니다.

    import torch
    from transformers import (
        AutoModelForCausalLM,
        AutoTokenizer,
        BitsAndBytesConfig,
        TrainingArguments
    )
    from peft import LoraConfig, get_peft_model, TaskType
    from trl import SFTTrainer
    from datasets import load_dataset
    
    # ===== 1. 모델 설정 =====
    # 베이스 모델 선택 (Hugging Face Hub에서 다운로드)
    # 예시: meta-llama/Meta-Llama-3-8B, mistralai/Mistral-7B-v0.1 등
    BASE_MODEL = "mistralai/Mistral-7B-v0.1"  # 본인 용도에 맞게 변경
    OUTPUT_DIR = "./my-domain-model"
    
    # ===== 2. 4-bit 양자화 설정 (QLoRA의 핵심) =====
    bnb_config = BitsAndBytesConfig(
        load_in_4bit=True,               # 4-bit로 모델 로드
        bnb_4bit_use_double_quant=True,  # 이중 양자화로 메모리 추가 절약
        bnb_4bit_quant_type="nf4",       # NF4 타입 (QLoRA 논문 권장)
        bnb_4bit_compute_dtype=torch.bfloat16  # 계산은 bfloat16으로
    )
    
    # ===== 3. 모델 & 토크나이저 로드 =====
    print("모델 로딩 중... (처음엔 시간이 좀 걸려요)")
    model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL,
        quantization_config=bnb_config,
        device_map="auto",  # GPU 자동 할당
        trust_remote_code=True
    )
    model.config.use_cache = False  # 학습 시엔 꺼야 합니다
    
    tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL, trust_remote_code=True)
    tokenizer.pad_token = tokenizer.eos_token  # 패딩 토큰 설정
    tokenizer.padding_side = "right"  # 오른쪽 패딩 (중요!)
    
    # ===== 4. LoRA 어댑터 설정 =====
    lora_config = LoraConfig(
        task_type=TaskType.CAUSAL_LM,
        r=16,             # 랭크(rank) — 클수록 더 많이 학습, 메모리도 더 씀
        lora_alpha=32,    # 스케일링 파라미터 (보통 r의 2배)
        target_modules=[  # 어떤 레이어에 LoRA 적용할지
            "q_proj", "k_proj", "v_proj", "o_proj",
            "gate_proj", "up_proj", "down_proj"
        ],
        lora_dropout=0.05,
        bias="none"
    )
    
    model = get_peft_model(model, lora_config)
    model.print_trainable_parameters()  # 학습 파라미터 비율 확인
    # 출력 예시: trainable params: 20,185,088 || all params: 3,772,923,904 || trainable%: 0.5348
    
    # ===== 5. 학습 인자 설정 =====
    training_args = TrainingArguments(
        output_dir=OUTPUT_DIR,
        num_train_epochs=3,
        per_device_train_batch_size=4,
        gradient_accumulation_steps=4,  # 유효 배치 = 4 * 4 = 16
        learning_rate=2e-4,
        fp16=False,
        bf16=True,           # bfloat16 사용 (Ampere 이상 GPU)
        logging_steps=10,
        save_steps=100,
        warmup_ratio=0.03,
        lr_scheduler_type="cosine",
        report_to="none",    # wandb 쓰시면 "wandb"로 변경
        optim="paged_adamw_8bit"  # 메모리 효율적인 옵티마이저
    )
    
    # ===== 6. 데이터셋 로드 =====
    # 앞서 준비한 데이터셋 사용
    dataset = load_dataset("json", data_files="my_domain_data.json", split="train")
    dataset = dataset.map(format_instruction)  # 앞서 정의한 함수
    
    # ===== 7. SFT Trainer로 학습 시작 =====
    trainer = SFTTrainer(
        model=model,
        train_dataset=dataset,
        peft_config=lora_config,
        dataset_text_field="text",
        max_seq_length=2048,
        tokenizer=tokenizer,
        args=training_args,
    )
    
    print("학습 시작!")
    trainer.train()
    
    # ===== 8. 어댑터 저장 =====
    trainer.model.save_pretrained(OUTPUT_DIR)
    tokenizer.save_pretrained(OUTPUT_DIR)
    print(f"\n✅ 학습 완료! 모델 저장 위치: {OUTPUT_DIR}")

    여기서 중요한 포인트! r 값(랭크)을 너무 크게 잡으면 과적합(overfitting) 위험이 있고, 너무 작으면 학습이 덜 됩니다. 저는 도메인 데이터 양에 따라 데이터 1000개 이하면 r=8, 5000개 이상이면 r=16~32를 주로 씁니다.

    학습된 모델로 추론하기

    학습이 끝났으면 실제로 써봐야죠! 저장된 LoRA 어댑터를 베이스 모델에 합쳐서 추론하는 코드입니다.

    from transformers import AutoModelForCausalLM, AutoTokenizer
    from peft import PeftModel
    import torch
    
    BASE_MODEL = "mistralai/Mistral-7B-v0.1"
    ADAPTER_PATH = "./my-domain-model"
    
    # 베이스 모델 로드 (추론 시엔 양자화 없이 할 수도 있어요)
    tokenizer = AutoTokenizer.from_pretrained(BASE_MODEL)
    model = AutoModelForCausalLM.from_pretrained(
        BASE_MODEL,
        torch_dtype=torch.float16,
        device_map="auto"
    )
    
    # LoRA 어댑터 합치기
    model = PeftModel.from_pretrained(model, ADAPTER_PATH)
    model = model.merge_and_unload()  # 어댑터를 모델에 완전히 병합
    model.eval()
    
    def generate_response(instruction, input_text="", max_new_tokens=512):
        if input_text:
            prompt = f"### Instruction:\n{instruction}\n\n### Input:\n{input_text}\n\n### Response:\n"
        else:
            prompt = f"### Instruction:\n{instruction}\n\n### Response:\n"
        
        inputs = tokenizer(prompt, return_tensors="pt").to(model.device)
        
        with torch.no_grad():
            outputs = model.generate(
                **inputs,
                max_new_tokens=max_new_tokens,
                temperature=0.7,
                do_sample=True,
                top_p=0.9,
                repetition_penalty=1.1  # 반복 방지
            )
        
        response = tokenizer.decode(outputs[0], skip_special_tokens=True)
        # 프롬프트 부분 제거하고 응답만 반환
        return response.split("### Response:")[-1].strip()
    
    # 테스트
    response = generate_response("우리 회사의 배포 프로세스를 설명해줘")
    print(response)

    ⚠️ 실제로 겪은 트러블슈팅 모음

    이 섹션이 사실 제일 중요할 수도 있어요. 저처럼 삽질 안 하시라고 정리해봤습니다.

    문제 1: CUDA out of memory 에러

    가장 흔한 문제입니다. 해결책은 이렇습니다:

    • per_device_train_batch_size를 줄이고 gradient_accumulation_steps를 늘리세요 (유효 배치 크기는 유지하면서)
    • max_seq_length를 줄여보세요. 2048 → 1024로만 줄여도 메모리가 확 줄어듭니다
    • 학습 전에 torch.cuda.empty_cache() 호출

    문제 2: 모델이 Instruction을 무시하고 이상한 말을 함

    데이터 포맷이 안 맞는 경우가 대부분입니다. 학습 데이터의 프롬프트 형식과 추론 시 프롬프트 형식이 완전히 동일해야 합니다. 공백 하나, 줄바꿈 하나도 다르면 모델이 헷갈려해요.

    문제 3: Loss가 안 줄어듦

    Learning rate 문제일 가능성이 높습니다. 2e-4가 일반적이지만, 데이터가 적으면 1e-4로 낮춰보세요. Warmup 비율도 0.03 → 0.05로 올려보는 것도 방법입니다.

    문제 4: 학습 후 모델이 기존 능력을 잃음 (Catastrophic Forgetting)

    이건 LoRA의 장점 중 하나인데, 그래도 데이터에 너무 도메인 특화 내용만 있으면 발생할 수 있어요. 일반 대화 데이터를 10~20% 섞어주면 많이 해결됩니다. Alpaca 데이터셋이나 OpenHermes 같은 공개 데이터셋에서 일부 샘플링해서 섞어주세요.

    ▲ 파인튜닝 전후 도메인 특화 질문에 대한 응답 비교 — 학습 후 도메인 용어와 맥락을 정확히 이해하는 것을 확인할 수 있습니다.

    🎉 결과 확인 — 드디어 됐다!

    제가 실제로 사내 기술 문서 약 2,000개로 파인튜닝했을 때 결과를 공유하면, 파인튜닝 전에는 우리 팀 특유의 시스템 이름이나 약어를 물어보면 모르거나 엉뚱한 답을 했는데, 파인튜닝 후에는 정확하게 내부 컨텍스트에 맞는 답변을 해주더라고요. 특히 “이 에러 코드가 뭔 뜻이야?”류의 질문에서 차이가 극명했습니다.

    모델 평가는 정성적 평가(직접 질문해보기)와 더불어, 보류해둔 테스트 셋으로 간단히 정량 평가도 해보세요. 저는 ROUGE 스코어보다는 실제 사용자 피드백이 더 의미 있다고 생각하는 편이에요.

    # 학습된 모델 파일 구조 확인
    ls -la ./my-domain-model/
    # adapter_config.json    — LoRA 설정 정보
    # adapter_model.safetensors  — 학습된 가중치 (수십~수백 MB)
    # tokenizer.json
    # tokenizer_config.json
    # special_tokens_map.json
    
    # 파일 크기 확인 (베이스 모델 대비 훨씬 작습니다)
    du -sh ./my-domain-model/
    # 예시: 약 80~300MB (베이스 모델은 수 GB인 것과 비교)

    이게 LoRA의 또 다른 장점인데요, 어댑터 파일만 배포하면 되니까 용량이 매우 작습니다. 베이스 모델은 공유하고 어댑터만 바꿔끼는 방식으로 여러 도메인 모델을 관리할 수 있어서 정말 편해요.

    ▲ LoRA/QLoRA 파인튜닝 핵심 파라미터 요약 — r, alpha, target_modules 등 주요 설정값 선택 가이드

    정리 — 오늘 배운 것들

    처음에 “GPU 한 장으로 LLM 파인튜닝이 가능할까?”라는 의심으로 시작했는데, QLoRA 덕분에 충분히 가능하다는 걸 직접 확인했습니다. 핵심만 정리해볼게요.

    • ✅ QLoRA는 소비자급 GPU로도 수십억 파라미터 모델을 파인튜닝할 수 있게 해줍니다
    • ✅ 데이터 품질이 양보다 중요합니다. 적더라도 잘 만든 데이터가 훨씬 낫습니다
    • ✅ LoRA rank(r)는 데이터 양에 맞게 조절하세요. 처음엔 r=16이 무난합니다
    • ✅ 프롬프트 형식을 학습과 추론에서 완전히 동일하게 유지하세요
    • ✅ 일반 데이터를 일부 섞어서 Catastrophic Forgetting을 방지하세요

    다음 글에서는 이렇게 만든 모델을 Ollama나 vLLM으로 서빙(serving)하는 방법을 다룰 예정입니다. API 서버로 띄워서 실제 서비스에 붙이는 과정까지 이어서 정리해드릴게요. 이전 글에서 RAG 구축 과정을 다뤘으니 참고하시면 파인튜닝과 함께 쓰는 방법도 이해하기 쉬울 거예요.

    궁금한 점은 댓글로 남겨주세요. 저도 아직 공부 중이라 같이 논의하면 더 좋을 것 같습니다 😊

    자주 묻는 질문 (FAQ)

    Q. 최소 얼마나 많은 데이터가 필요한가요?
    A. 경험상 최소 500~1000개 정도의 고품질 instruction 데이터면 의미 있는 파인튜닝이 됩니다. 물론 많을수록 좋지만, 품질이 더 중요합니다.
    Q. 어떤 베이스 모델을 선택하는 게 좋을까요?
    A. 한국어 도메인이라면 한국어 데이터로 사전학습된 모델을 베이스로 쓰는 게 유리합니다. EEVE-Korean, EXAONE 등 국내에서 공개한 모델들도 좋은 선택지입니다. 영어 도메인이라면 Mistral 7B나 LLaMA 3 8B가 무난합니다.
    Q. 파인튜닝 한 번에 얼마나 걸리나요?
    A. 데이터 양과 GPU 성능에 따라 다르지만, RTX 3090 기준 1000개 데이터, 3 에폭 학습에 1~2시간 정도 예상하시면 됩니다.
    Q. LoRA 어댑터를 베이스 모델에 완전히 병합해야 하나요?
    A. 추론 시 편의를 위해 merge_and_unload()로 병합할 수 있지만, 어댑터를 분리 유지하면 나중에 다른 어댑터로 교체하기 쉽습니다. 여러 도메인 모델을 관리한다면 분리 유지를 추천합니다.