13년차의 서버실

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

[태그:] 오픈소스 LLM

  • [AI] Ollama 1년 실사용 회고: 로컬 LLM 개발, 기대와 현실 사이

    [AI] Ollama 1년 실사용 회고: 로컬 LLM 개발, 기대와 현실 사이

    Ollama 1년 실사용 회고: 로컬 LLM 개발, 기대와 현실 사이

    안녕하세요, 13년차 서버실 지킴이, 13년차의 서버실입니다. 오늘은 제가 지난 1년간 홈랩에서 직접 경험하고 삽질했던 Ollama(올라마)에 대한 솔직한 회고를 들려드리려고 합니다. 최근 몇 년간 AI, 특히 LLM(Large Language Model, 거대 언어 모델)이 IT 업계를 뜨겁게 달구고 있잖아요? 인프라 엔지니어로서 저도 이 거대한 흐름을 놓칠 수 없었습니다.

    처음에는 GPT 같은 클라우드 기반 LLM을 사용하면서 그 성능에 감탄했지만, 문득 이런 생각이 들더라고요. ‘내가 만든 서비스에 LLM을 붙이려면 비용은 어떻게 감당하지? 그리고 중요한 건, 민감한 우리 회사 데이터를 외부 서비스에 보내도 괜찮을까?’ ⚠️ 이런 고민을 하다 보면 결국 로컬 LLM(Local LLM)으로 눈을 돌리게 됩니다. 하지만 로컬에서 LLM을 돌린다는 게 말처럼 쉽지 않았어요. 복잡한 환경 설정, 모델 다운로드, 의존성 관리… 솔직히 엄두가 안 났거든요.

    그러다 1년 전쯤, Ollama라는 녀석을 처음 만났습니다. “어? 이거 뭔가 다르다!” 싶더라고요. 설치도 간편하고, 모델 관리도 쉽다는 얘기에 ‘이거다!’ 싶어서 바로 홈랩에 세팅하고 이것저것 굴려봤습니다. 지난 1년간의 경험을 바탕으로, Ollama를 통한 로컬 LLM 개발이 과연 어떤 기대를 충족시켜주고 또 어떤 현실적인 벽에 부딪혔는지, 제 삽질 경험과 함께 솔직하게 풀어보겠습니다. 혹시 로컬 LLM 개발에 관심 있으신 분들이라면 제 이야기가 조금이나마 도움이 되었으면 좋겠네요. 😊

    Ollama 아키텍처 및 로컬 LLM 실행 흐름 다이어그램 (Ollama, 로컬 LLM 키워드 포함)

    Ollama는 복잡한 LLM 모델을 마치 Docker 이미지처럼 손쉽게 다운로드하고 실행할 수 있도록 돕는 도구입니다. 이 그림은 Ollama를 통해 로컬 환경에서 LLM이 어떻게 구동되는지 간략하게 보여줍니다.

    💡 Ollama, 로컬 LLM의 진입 장벽을 낮추다

    Ollama가 정확히 무엇인지부터 짚고 넘어갈까요? Ollama(올라마)는 쉽게 말해, 여러분의 컴퓨터에서 오픈소스 LLM(Open-source Large Language Model)을 아주 쉽게 설치하고 실행할 수 있도록 도와주는 도구입니다. 기존에는 로컬 LLM을 돌리려면 Python 환경 설정부터 시작해서 PyTorch, CUDA 같은 라이브러리 설치, 모델 가중치(weights) 다운로드, 그리고 복잡한 코드 작성까지, 정말 해야 할 일이 많았거든요. 저도 처음엔 이 과정에서만 몇 번이나 포기할 뻔했습니다.

    근데 Ollama는 이런 복잡한 과정을 컨테이너 방식으로 단순화시켜 줍니다. 마치 Docker(도커)로 이미지를 다운받아 실행하듯이, <code>ollama pull [모델명] 명령 하나로 원하는 로컬 LLM 모델을 내려받고, ollama run [모델명] 명령으로 바로 실행할 수 있게 해주는 거죠. 이게 진짜 혁신적이라고 느꼈어요. 🚀 덕분에 저 같은 인프라 엔지니어는 물론, AI 개발에 막 입문하는 분들도 훨씬 쉽게 LLM을 만져볼 수 있게 된 겁니다.

    Ollama는 다양한 오픈소스 LLM들을 지원합니다. 대표적으로 Meta의 Llama 2(라마 2), Mistral AI의 Mistral(미스트랄), Google의 Gemma(젬마) 등 유명 모델들을 공식적으로 제공하고 있어요. 게다가 커뮤니티에서 만들어진 수많은 모델들도 쉽게 사용할 수 있어서 선택의 폭이 엄청 넓습니다. 로컬 LLM 개발 환경을 구축하고 싶다면 Ollama는 정말 탁월한 선택지라고 자신 있게 말씀드릴 수 있습니다.

    🛠️ 홈랩에서 Ollama 설치하고 첫 LLM 실행하기

    자, 그럼 실제로 제 홈랩에서 Ollama를 어떻게 설치하고 첫 LLM을 실행했는지 보여드릴게요. 저는 주로 Linux 환경을 사용하는데, macOS나 Windows(WSL2)에서도 비슷하게 적용할 수 있습니다. 설치 과정은 정말 간단해서 놀라실 거예요.

    1. Ollama 설치

    터미널을 열고 다음 명령어를 입력하면 끝입니다. 스크립트가 알아서 필요한 것들을 설치해줍니다. 💡

    curl -fsSL https://ollama.com/install.sh | sh
    

    설치가 완료되면 ollama 명령어를 입력해서 정상적으로 설치되었는지 확인할 수 있습니다.

    ollama
    

    아마 사용 가능한 명령어 목록이 주르륵 뜰 거예요. 🎉

    2. LLM 모델 다운로드

    다음으로 원하는 로컬 LLM 모델을 다운로드합니다. 저는 가장 먼저 Llama 2(라마 2)를 다운로드해봤어요. 7B(70억 파라미터) 모델이 로컬에서 돌려보기 적당하거든요.

    ollama pull llama2
    

    모델 크기에 따라 시간이 좀 걸릴 수 있습니다. 제 홈랩 인터넷이 광랜이라 금방 내려받았지만, 모델이 몇 GB씩 하는 경우도 많으니 인내심을 가지세요. ㅎㅎ

    3. LLM 모델 실행 및 대화

    모델 다운로드가 완료되면 바로 실행해볼 수 있습니다.

    ollama run llama2
    

    명령어를 입력하면 터미널에서 Llama 2와 대화를 시작할 수 있습니다. 처음으로 로컬에서 제가 질문한 내용에 LLM이 답변하는 걸 봤을 때의 그 감동이란… 직접 경험해보지 않으면 모릅니다! 🤩

    >>> Tell me a joke.
    Why don't scientists trust atoms?
    Because they make up everything!
    

    4. 프로그래밍 연동 (Python 예시)

    Ollama는 REST API를 제공해서 다양한 프로그래밍 언어에서 쉽게 연동할 수 있습니다. 저는 Python으로 간단한 챗봇을 만들어보면서 로컬 LLM 연동 테스트를 해봤어요.

    import ollama
    
    # 로컬 Ollama 서버에 요청
    response = ollama.chat(model='llama2', messages=[
      {
        'role': 'user',
        'content': 'Why is the sky blue?',
      },
    ])
    print(response['message']['content'])
    

    이렇게 코드로 쉽게 로컬 LLM의 기능을 활용할 수 있다니, 개발자 입장에서 정말 편하더라고요. AI 개발 환경 구축이 이렇게 쉬워지다니, 격세지감을 느꼈습니다.

    Ollama 모델 다운로드 및 실행 터미널 스크린샷 (Ollama 설치, 로컬 LLM 개발 키워드 포함)

    Ollama를 설치하고 Llama 2 모델을 다운로드하여 실행하는 과정을 터미널 스크린샷으로 담아봤습니다. 몇 줄의 간단한 명령어로 로컬 LLM 개발 환경이 구축되는 것을 직접 확인할 수 있습니다.

    ⚠️ 1년간의 삽질과 트러블슈팅: 기대와 현실 사이

    Ollama가 로컬 LLM의 진입 장벽을 낮춰준 것은 분명하지만, 그렇다고 마냥 순탄하기만 했던 건 아닙니다. 1년 동안 삽질도 많이 했고, 현실적인 제약에 부딪히면서 ‘아, 이게 아직은 갈 길이 멀구나’ 하는 생각도 많이 했어요.

    1. 하드웨어 제약: GPU와 VRAM의 중요성

    가장 크게 느낀 점은 역시 하드웨어 제약입니다. 특히 GPU(Graphics Processing Unit, 그래픽 처리 장치)의 중요성은 아무리 강조해도 지나치지 않아요. 제 홈랩에는 NVIDIA GeForce RTX 3060 12GB 모델이 장착되어 있습니다. 처음에는 이 정도면 꽤 좋은 GPU라고 생각했는데, 로컬 LLM을 돌려보니 바로 한계를 느꼈습니다.

    작은 모델(예: Llama 2 7B)은 그럭저럭 돌아가지만, 조금만 더 큰 모델(예: 13B 이상)이나 여러 모델을 동시에 띄우려고 하면 VRAM(Video RAM, 비디오 램)이 순식간에 동나 버립니다. VRAM이 부족하면 로컬 LLM은 CPU(Central Processing Unit, 중앙 처리 장치)로 동작하게 되는데, 그러면 응답 속도가 현저히 느려져서 사실상 실사용이 어렵습니다. 답변 하나 받는 데 몇 분씩 걸리니 답답해서 못 쓰겠더라고요. 😥

    그래서 저는 주로 Quantization(양자화)된 모델을 사용했습니다. 양자화는 모델의 정밀도를 낮춰 크기와 VRAM 사용량을 줄이는 기법인데, Q4_0, Q8_0 같은 버전들이 대표적입니다. 성능 저하가 있긴 하지만, 제 RTX 3060으로는 양자화된 13B 모델도 버겁게 돌려야 했어요. 7B Q4_0 모델 정도가 그나마 쾌적하게 느껴지는 마지노선이었습니다.

    2. 발열과 소음

    홈랩에서 밤늦게 로컬 LLM을 돌리다 보면 GPU 팬 소리가 비행기 이륙하는 소리처럼 커지곤 했습니다. 😅 높은 GPU 사용률은 곧 높은 발열로 이어지고, 팬은 그 열을 식히기 위해 미친 듯이 돌아갑니다. 여름철에는 에어컨을 켜지 않으면 방 온도가 훅 올라갈 정도였어요. 조용한 환경을 선호하는 분들에게는 이것도 큰 단점으로 다가올 수 있습니다.

    3. 클라우드 LLM과의 성능 차이

    아무리 로컬 LLM이 발전했다고 해도, 클라우드에서 제공하는 최신, 최고 성능의 LLM(예: GPT-4, Claude 3)과는 아직 넘을 수 없는 벽이 존재합니다. 응답 속도, 답변의 품질, 복잡한 추론 능력 등 여러 면에서 차이가 컸어요. 오픈소스 LLM도 빠르게 발전하고 있지만, 최상위 모델들은 여전히 막대한 컴퓨팅 자원을 요구하거든요. 그래서 저는 중요한 작업이나 복잡한 문제는 클라우드 LLM을, 개인 학습이나 아이디어 검증은 Ollama를 활용하는 방식으로 병행하고 있습니다.

    GPU VRAM 및 CPU 사용률 모니터링 대시보드 (Ollama 성능, GPU 키워드 포함)

    로컬 LLM 개발에서 가장 중요한 요소 중 하나는 바로 하드웨어입니다. 특히 GPU의 VRAM은 모델의 성능과 직결되죠. 이 이미지는 제가 Ollama로 로컬 LLM을 실행했을 때 GPU와 CPU 사용률을 모니터링한 화면입니다.

    ✅ Ollama로 얻은 것과 아쉬웠던 점

    1년 동안 Ollama를 사용하면서 느꼈던 점들을 장단점으로 나눠서 정리해봤습니다. 로컬 LLM 사용 후기를 찾으시는 분들에게 객관적인 정보가 되었으면 좋겠네요.

    좋았던 점 (기대 이상)

    1. LLM 개념 학습 및 실험 환경 제공: 가장 큰 장점이라고 생각합니다. LLM이 어떻게 작동하는지, 프롬프트 엔지니어링은 어떻게 해야 하는지 등을 실제 로컬 LLM 모델을 직접 돌려보면서 체득할 수 있었습니다. 시행착오를 거치며 배우는 재미가 쏠쏠했어요.
    2. 데이터 프라이버시 유지: 민감한 데이터를 외부로 유출할 걱정 없이 내부망에서 로컬 LLM을 활용할 수 있다는 점은 큰 메리트입니다. 특히 기업 환경에서는 이 부분이 매우 중요하겠죠.
    3. 오프라인 환경 개발 가능: 인터넷이 연결되지 않은 환경에서도 로컬 LLM을 활용한 개발 및 테스트가 가능했습니다. 물론 모델 다운로드는 필요하지만요.
    4. 다양한 오픈소스 모델 탐색: Llama 2, Mistral, Gemma 외에도 Code Llama, Orca, TinyLlama 등 수많은 오픈소스 모델들을 쉽게 다운로드하고 비교 실험해볼 수 있었습니다. 각각의 모델이 어떤 특징을 가지는지 파악하는 데 큰 도움이 되었어요.

    아쉬웠던 점 (현실의 벽)

    1. 여전히 높은 하드웨어 요구사항: 앞서 언급했듯이, ‘괜찮은’ 성능을 내려면 고사양 GPU가 필수적입니다. 일반적인 게이밍 PC로는 로컬 LLM의 한계가 명확하더라고요.
    2. 대규모 모델 실행의 어려움: 70B(700억 파라미터) 이상의 로컬 LLM 모델은 실사용하기 어렵습니다. 최소 24GB 이상의 VRAM이 필요하며, 고가의 전문가용 GPU(예: A6000, H100)가 아니면 엄두를 내기 힘들죠.
    3. 클라우드 서비스 대비 부족한 성능: 답변의 품질, 속도, 일관성 등 여러 면에서 클라우드 LLM 서비스에 비해 아쉬움이 남습니다. 특히 장문의 글을 생성하거나 복잡한 추론을 요구할 때 차이가 두드러졌어요.
    4. 모델 간 전환 시 부하: 여러 로컬 LLM 모델을 동시에 띄우는 것도 VRAM 문제로 어렵고, 모델을 전환할 때마다 VRAM에 다시 로딩하는 과정이 필요해서 시간이 걸립니다.
    Ollama 장점과 한계 비교 인포그래픽 (Ollama 장점, 로컬 LLM 한계 키워드 포함)

    1년간 Ollama를 사용하며 느낀 점들을 장점과 단점으로 정리해봤습니다. 이 인포그래픽은 로컬 LLM 개발 환경으로서 Ollama가 제공하는 가치와 마주하게 되는 현실적인 한계를 시각적으로 보여줍니다.

    🚀 Ollama, 앞으로의 방향과 나의 활용법

    결론적으로, Ollama는 로컬 LLM 개발의 진입 장벽을 혁신적으로 낮춰준 아주 훌륭한 도구입니다. 제가 13년차 인프라 엔지니어로서 새로운 기술을 경험하고 학습하는 데 정말 큰 도움을 주었습니다. 🎉 하지만 아직은 명확한 한계도 가지고 있다는 점을 솔직히 말씀드리고 싶어요.

    그럼에도 불구하고 Ollama와 같은 도구들의 발전은 앞으로도 계속될 것이고, GPU 기술도 점점 더 발전할 것이기에 로컬 LLM의 미래는 밝다고 생각합니다. 제 홈랩에서도 지속적으로 Ollama를 활용하여 다음과 같은 방식으로 AI 개발 환경을 운영할 계획입니다.

    • 개인 학습 및 개념 증명(POC): 새로운 오픈소스 LLM이 나오면 Ollama로 빠르게 테스트해보고, 간단한 아이디어를 검증하는 용도로 사용.
    • RAG(Retrieval Augmented Generation) 실험: 사내 문서나 개인 데이터를 기반으로 로컬 LLM이 답변하도록 하는 RAG 시스템을 로컬에서 구축하고 실험하는 데 활용. (이 부분은 다음 글에서 자세히 다뤄볼까 합니다!)
    • 데이터 프라이버시가 중요한 소규모 서비스: 외부망 노출이 어려운 특정 기능에 로컬 LLM을 연동할 필요가 있을 때.

    물론 대규모 서비스나 높은 성능, 복잡한 추론 능력이 필요한 경우에는 여전히 클라우드 기반 LLM 서비스가 유리할 겁니다. 중요한 것은 각자의 상황과 필요에 맞게 로컬 LLM과 클라우드 LLM을 적절히 조합하여 사용하는 지혜가 필요하다는 점입니다.

    13년차 인프라 엔지니어의 Ollama 1년 실사용 후기, 어떠셨나요? 로컬 LLM 개발에 대한 기대와 현실 사이에서 저와 비슷한 고민을 하셨던 분들이라면, 제 경험이 조금이나마 길잡이가 되었으면 좋겠습니다. 다음에는 Ollama와 RAG를 연동하는 방법에 대해 더 깊이 있는 내용을 다뤄볼 예정이니, 많은 기대 부탁드립니다! 😉

    미래 로컬 LLM 개발 환경 상상도 (AI 개발 미래, 로컬 LLM 전망 키워드 포함)

    Ollama는 로컬 LLM 개발의 가능성을 열어주었지만, 아직 가야 할 길이 멀다고 생각합니다. 미래에는 더욱 강력한 개인 워크스테이션과 효율적인 로컬 LLM 모델로, 지금보다 훨씬 더 풍부한 경험을 할 수 있기를 기대해봅니다.

  • [AI] vLLM 활용 가이드: LLM 추론 성능 최적화 및 비용 절감 전략

    LLM 추론 비용, 이대로 괜찮을까요?

    요즘 LLM(Large Language Model, 대형 언어 모델)을 서비스에 붙이는 팀들이 정말 많아졌죠. 근데 막상 직접 추론 서버를 운영해보면 첫 번째 벽이 바로 비용이거든요. GPU 한 장 풀로 돌리면서 초당 몇 개 요청도 못 받는 상황, 저도 경험해봤습니다. 처음에 Hugging Face의 기본 파이프라인으로 모델을 띄웠을 때 처리량이 너무 낮아서 ‘이걸 어떻게 서비스에 쓰지?’ 싶었거든요.

    그때 동료한테 추천받은 게 바로 vLLM이었습니다. 처음엔 그냥 또 다른 추론 프레임워크겠지 했는데, 써보고 나서 진짜 놀랐어요. 같은 GPU, 같은 모델인데 처리량이 확 달라지더라고요. 이번 글에서는 vLLM이 뭔지, 어떻게 설치하고 실제로 어떻게 쓰는지, 그리고 LLM 추론 성능 최적화와 비용 절감을 위해 어떤 설정을 만져야 하는지 제가 직접 경험한 내용을 바탕으로 정리해드리겠습니다.

    ▲ vLLM의 핵심 아키텍처 — PagedAttention과 Continuous Batching이 어떻게 GPU 메모리를 효율적으로 활용하는지 보여주는 전체 구성도

    vLLM이 뭔데 이렇게 빠를까요?

    기존 LLM 추론의 문제점

    쉽게 말해서, 기존 LLM 추론 방식은 GPU 메모리를 엄청 낭비하는 구조였어요. 각 요청마다 KV Cache(Key-Value Cache, 어텐션 연산의 중간 결과를 저장하는 공간)를 미리 크게 잡아놓거든요. 요청마다 최대 시퀀스 길이만큼 메모리를 예약해두니까, 실제로 짧은 답변만 생성해도 긴 메모리가 낭비되는 거죠. 이걸 메모리 단편화(Memory Fragmentation) 문제라고 부릅니다.

    배치 처리(Batching)도 문제였어요. 여러 요청을 동시에 처리하려면 길이를 맞춰야 하는데, 길이가 제각각인 요청들을 묶으면 짧은 것들은 GPU가 쉬면서 기다리는 상황이 생기거든요. GPU 활용률이 뚝뚝 떨어지죠.

    vLLM의 핵심 기술: PagedAttention

    vLLM은 UC Berkeley에서 개발된 오픈소스 LLM 추론 엔진입니다. 핵심은 PagedAttention이라는 기술이에요. OS의 가상 메모리(Virtual Memory) 페이징 개념을 KV Cache에 적용한 건데요, 메모리를 고정 크기의 블록(Block)으로 나눠서 필요할 때만 할당하고 공유도 가능하게 만든 거예요.

    • PagedAttention: KV Cache를 페이지 단위로 관리 → 메모리 낭비 최소화
    • Continuous Batching(연속 배치): 요청이 끝나는 즉시 새 요청을 삽입 → GPU 유휴 시간 최소화
    • Tensor Parallelism(텐서 병렬화): 여러 GPU에 모델을 분산 → 대형 모델도 처리 가능
    • OpenAI 호환 API 서버: 기존 OpenAI SDK 코드를 거의 그대로 재사용 가능

    이 조합 덕분에 같은 하드웨어에서 기존 대비 훨씬 높은 처리량을 낼 수 있는 거예요. 실제로 써보면 체감이 확실하게 됩니다.

    vLLM 설치: 환경 세팅부터 차근차근

    사전 요구사항 확인

    설치 전에 먼저 환경부터 체크해야 해요. 저도 처음에 이 부분 대충 넘겼다가 삽질을 좀 했습니다.

    • Python 3.8 이상 (3.10 권장)
    • CUDA 11.8 이상 지원 NVIDIA GPU
    • CUDA Toolkit 설치 확인: nvcc --version
    • 충분한 GPU VRAM (7B 모델 기준 최소 16GB 권장)

    ⚠️ 주의: AMD GPU도 ROCm을 통해 지원되지만, NVIDIA CUDA 환경에 비해 안정성이 다를 수 있어요. 프로덕션 환경이라면 NVIDIA를 권장합니다.

    pip로 설치하기

    가장 간단한 방법은 pip 설치입니다. 가상환경을 쓰는 거 잊지 마세요!

    # 가상환경 생성 및 활성화
    python -m venv vllm-env
    source vllm-env/bin/activate  # Windows: vllm-env\Scripts\activate
    
    # pip 업그레이드
    pip install --upgrade pip
    
    # vLLM 설치 (CUDA 버전에 맞게)
    pip install vllm
    
    # 설치 확인
    python -c "import vllm; print(vllm.__version__)"

    Docker로 설치하기 (권장)

    프로덕션 환경이라면 Docker 이미지를 쓰는 게 훨씬 편해요. 의존성 충돌 걱정이 없거든요. 저도 홈랩에서는 Docker로 돌리고 있습니다.

    # vLLM 공식 Docker 이미지로 서버 실행 예시
    docker run --runtime nvidia --gpus all \
        -v ~/.cache/huggingface:/root/.cache/huggingface \
        -p 8000:8000 \
        --ipc=host \
        vllm/vllm-openai:latest \
        --model meta-llama/Llama-2-7b-chat-hf

    💡 팁: --ipc=host 옵션은 공유 메모리 관련 오류를 막아줘요. 빼먹으면 텐서 병렬화 쓸 때 오류가 날 수 있으니 꼭 넣어주세요.

    ▲ vLLM 서버 실행 후 터미널 출력 화면 — GPU 메모리 할당 현황과 서버 시작 로그를 확인할 수 있습니다

    vLLM 사용법: 실전 추론 서버 구성

    1단계: 기본 API 서버 실행

    vLLM의 진짜 강점 중 하나가 OpenAI 호환 API 서버를 바로 띄울 수 있다는 거예요. 기존에 OpenAI API를 쓰던 코드를 엔드포인트만 바꿔서 그대로 쓸 수 있거든요. 이거 처음 알았을 때 진짜 편하다 싶었어요.

    # OpenAI 호환 API 서버 실행
    python -m vllm.entrypoints.openai.api_server \
        --model meta-llama/Llama-2-7b-chat-hf \
        --host 0.0.0.0 \
        --port 8000 \
        --max-model-len 4096

    2단계: Python 코드로 직접 추론

    API 서버 없이 Python 코드 안에서 직접 vLLM을 쓰는 방법도 있어요. 배치 처리나 파이프라인 구성할 때 더 유연하게 쓸 수 있습니다.

    from vllm import LLM, SamplingParams
    
    # 모델 로드
    llm = LLM(model="meta-llama/Llama-2-7b-chat-hf")
    
    # 샘플링 파라미터 설정
    sampling_params = SamplingParams(
        temperature=0.7,      # 창의성 조절 (0=결정적, 1=창의적)
        top_p=0.9,            # nucleus sampling
        max_tokens=512,       # 최대 생성 토큰 수
        stop=["", "[INST]"]  # 정지 토큰
    )
    
    # 프롬프트 목록 (배치 처리)
    prompts = [
        "[INST] 파이썬에서 리스트와 튜플의 차이점을 설명해줘 [/INST]",
        "[INST] 도커 컨테이너와 가상머신의 차이점은? [/INST]",
        "[INST] REST API와 GraphQL의 장단점을 비교해줘 [/INST]",
    ]
    
    # 추론 실행
    outputs = llm.generate(prompts, sampling_params)
    
    # 결과 출력
    for output in outputs:
        prompt = output.prompt
        generated_text = output.outputs[0].text
        print(f"프롬프트: {prompt[:50]}...")
        print(f"생성 결과: {generated_text}")
        print("-" * 50)

    3단계: OpenAI SDK로 API 서버 호출

    서버를 띄웠다면 기존 OpenAI SDK 코드를 그대로 쓸 수 있어요. base_url만 바꿔주면 됩니다.

    from openai import OpenAI
    
    # vLLM 서버를 가리키도록 설정
    client = OpenAI(
        api_key="token-abc123",  # vLLM은 아무 값이나 넣어도 됨
        base_url="http://localhost:8000/v1",
    )
    
    # 채팅 완성 요청
    response = client.chat.completions.create(
        model="meta-llama/Llama-2-7b-chat-hf",
        messages=[
            {"role": "system", "content": "당신은 친절한 인프라 엔지니어입니다."},
            {"role": "user", "content": "Kubernetes와 Docker의 관계를 설명해주세요."},
        ],
        max_tokens=512,
        temperature=0.7,
    )
    
    print(response.choices[0].message.content)

    LLM 추론 성능 최적화: 핵심 파라미터 튜닝

    주요 최적화 옵션 비교

    여기서 중요한 포인트! vLLM 서버 실행 시 옵션을 어떻게 주느냐에 따라 성능이 크게 달라집니다. 제가 여러 조합을 테스트해보면서 정리한 표예요.

    옵션 설명 권장 값 영향
    --gpu-memory-utilization GPU 메모리 사용 비율 0.85~0.90 처리량 ↑, 너무 높으면 OOM
    --max-num-batched-tokens 배치당 최대 토큰 수 4096~8192 처리량 ↑, 메모리 사용 ↑
    --max-num-seqs 동시 처리 시퀀스 수 128~256 동시성 ↑, 지연 시간 ↑
    --tensor-parallel-size 텐서 병렬화 GPU 수 GPU 개수 대형 모델 지원
    --quantization 양자화 방식 awq / gptq 메모리 ↓, 속도 ↑
    --dtype 데이터 타입 bfloat16 속도 ↑, 정밀도 약간 ↓

    양자화(Quantization)로 비용 절감하기

    LLM 비용 절감에서 가장 효과적인 방법 중 하나가 바로 양자화예요. 모델의 가중치를 더 낮은 비트로 표현해서 메모리 사용량을 줄이는 기법인데요, vLLM은 AWQ(Activation-aware Weight Quantization)와 GPTQ를 지원합니다.

    # AWQ 양자화 모델 사용 예시
    python -m vllm.entrypoints.openai.api_server \
        --model TheBloke/Llama-2-7B-Chat-AWQ \
        --quantization awq \
        --dtype half \
        --gpu-memory-utilization 0.85 \
        --max-model-len 4096
    
    # GPTQ 양자화 모델 사용 예시
    python -m vllm.entrypoints.openai.api_server \
        --model TheBloke/Llama-2-7B-Chat-GPTQ \
        --quantization gptq \
        --dtype float16

    저도 7B 모델을 AWQ 4비트로 양자화해서 쓰니까 메모리 사용량이 절반 가까이 줄더라고요. 품질 저하도 생각보다 크지 않아서 실제 서비스에 쓰기에 충분한 수준이었습니다.

    멀티 GPU 텐서 병렬화

    GPU가 여러 장 있다면 텐서 병렬화(Tensor Parallelism)를 활용할 수 있어요. 13B나 70B 같은 큰 모델을 단일 GPU에 올리기 힘들 때 진가를 발휘합니다.

    # 4개 GPU로 70B 모델 실행
    python -m vllm.entrypoints.openai.api_server \
        --model meta-llama/Llama-2-70b-chat-hf \
        --tensor-parallel-size 4 \
        --gpu-memory-utilization 0.85 \
        --max-model-len 4096 \
        --dtype bfloat16

    스트리밍 응답으로 사용자 경험 개선

    토큰이 생성되는 대로 바로바로 보내주는 스트리밍(Streaming)도 vLLM에서 쉽게 구현할 수 있어요. 사용자 입장에서 첫 응답까지 기다리는 시간이 줄어드니까 체감 성능이 확 좋아집니다.

    from openai import OpenAI
    
    client = OpenAI(
        api_key="token-abc123",
        base_url="http://localhost:8000/v1",
    )
    
    # 스트리밍 응답
    stream = client.chat.completions.create(
        model="meta-llama/Llama-2-7b-chat-hf",
        messages=[{"role": "user", "content": "쿠버네티스 Pod의 생명주기를 설명해줘"}],
        max_tokens=512,
        stream=True,  # 스트리밍 활성화
    )
    
    # 실시간으로 토큰 출력
    for chunk in stream:
        if chunk.choices[0].delta.content is not None:
            print(chunk.choices[0].delta.content, end="", flush=True)
    print()  # 마지막 줄바꿈

    ▲ vLLM 설정별 추론 성능 비교 — Continuous Batching, 양자화, 텐서 병렬화 적용 전후의 처리량(tokens/sec) 변화를 시각화한 차트

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

    문제 1: CUDA Out of Memory (OOM) 오류

    가장 흔한 문제예요. 처음 세팅할 때 저도 이거 때문에 한참 헤맸습니다.

    증상: torch.cuda.OutOfMemoryError: CUDA out of memory

    해결책:

    1. --gpu-memory-utilization을 0.85 이하로 낮추기
    2. --max-model-len을 줄여서 KV Cache 크기 제한
    3. --max-num-seqs를 줄여서 동시 처리 시퀀스 제한
    4. 양자화 모델 사용 고려
    # OOM 방지를 위한 보수적인 설정
    python -m vllm.entrypoints.openai.api_server \
        --model meta-llama/Llama-2-7b-chat-hf \
        --gpu-memory-utilization 0.80 \
        --max-model-len 2048 \
        --max-num-seqs 64

    문제 2: 모델 로딩이 너무 느림

    Hugging Face에서 모델을 매번 다운받으면 서버 재시작할 때마다 한세월이에요. 로컬 캐시를 제대로 설정해야 합니다.

    # Hugging Face 캐시 디렉토리 명시적 설정
    export HF_HOME=/data/huggingface/cache
    export TRANSFORMERS_CACHE=/data/huggingface/cache
    
    # 또는 --download-dir 옵션 사용
    python -m vllm.entrypoints.openai.api_server \
        --model meta-llama/Llama-2-7b-chat-hf \
        --download-dir /data/models

    문제 3: 멀티 GPU 환경에서 NCCL 오류

    텐서 병렬화를 쓸 때 NCCL(NVIDIA Collective Communications Library, GPU 간 통신 라이브러리) 관련 오류가 나는 경우가 있어요.

    해결책:

    # NCCL 디버그 로그 활성화 (원인 파악용)
    export NCCL_DEBUG=INFO
    
    # PCIe 환경에서 P2P 비활성화 시도
    export NCCL_P2P_DISABLE=1
    
    # IPC 모드 설정 (Docker 환경)
    # docker run 시 --ipc=host 옵션 필수

    문제 4: 특정 모델 로드 실패

    모든 모델이 vLLM에서 바로 돌아가지는 않아요. vLLM 공식 문서의 Supported Models 페이지에서 지원 모델을 확인할 수 있습니다. 지원하지 않는 모델은 별도 어댑터 작업이 필요하거나 아예 안 될 수도 있으니 먼저 확인하세요.

    성능 모니터링: 제대로 잘 돌아가고 있는지 확인하기

    vLLM 내장 메트릭 확인

    vLLM은 Prometheus(프로메테우스, 모니터링 시스템) 형식의 메트릭을 기본으로 노출해줘요. 서버 실행 후 /metrics 엔드포인트로 확인할 수 있습니다.

    # 메트릭 엔드포인트 확인
    curl http://localhost:8000/metrics
    
    # 주요 메트릭들:
    # vllm:num_requests_running     - 현재 처리 중인 요청 수
    # vllm:num_requests_waiting     - 대기 중인 요청 수
    # vllm:gpu_cache_usage_perc     - KV Cache 사용률 (%)
    # vllm:num_requests_finished    - 완료된 요청 수
    # vllm:e2e_request_latency      - 전체 요청 지연 시간

    간단한 성능 테스트 스크립트

    직접 성능을 테스트해보고 싶다면 아래 스크립트를 써보세요. 저도 새 설정을 적용할 때마다 이런 식으로 확인합니다.

    import time
    import asyncio
    import aiohttp
    
    async def send_request(session, prompt):
        """단일 요청 전송 및 시간 측정"""
        start = time.time()
        async with session.post(
            "http://localhost:8000/v1/completions",
            json={
                "model": "meta-llama/Llama-2-7b-chat-hf",
                "prompt": prompt,
                "max_tokens": 100,
            }
        ) as response:
            result = await response.json()
            elapsed = time.time() - start
            tokens = result["usage"]["completion_tokens"]
            return elapsed, tokens
    
    async def benchmark(num_requests=50):
        """동시 요청 벤치마크"""
        prompts = ["쿠버네티스란 무엇인가요?" for _ in range(num_requests)]
        
        async with aiohttp.ClientSession() as session:
            start_total = time.time()
            tasks = [send_request(session, p) for p in prompts]
            results = await asyncio.gather(*tasks)
            total_time = time.time() - start_total
        
        total_tokens = sum(r[1] for r in results)
        print(f"총 요청 수: {num_requests}")
        print(f"총 소요 시간: {total_time:.2f}초")
        print(f"총 생성 토큰: {total_tokens}")
        print(f"처리량: {total_tokens / total_time:.1f} tokens/sec")
        print(f"평균 지연: {sum(r[0] for r in results) / len(results):.2f}초")
    
    asyncio.run(benchmark())

    LLM 비용 절감 전략 총정리

    지금까지 다룬 내용을 비용 절감 관점에서 정리해볼게요. 결국 LLM 추론 비용은 GPU 사용 효율을 얼마나 높이느냐의 문제거든요.

    ▲ vLLM 기반 LLM 비용 절감 전략 요약 인포그래픽 — 양자화, 배치 최적화, GPU 활용률 향상을 통한 비용 절감 효과 비교

    전략 방법 기대 효과 주의사항
    양자화 적용 AWQ/GPTQ 4비트 모델 사용 메모리 50% 절감, 더 작은 GPU로 운영 가능 미세한 품질 저하 가능
    배치 최적화 Continuous Batching 활용 GPU 유휴 시간 최소화, 처리량 ↑ 지연 시간 약간 증가
    max-model-len 조정 실제 필요한 길이로 제한 KV Cache 절약, 더 많은 동시 요청 긴 컨텍스트 처리 불가
    작은 모델 선택 7B vs 13B 용도별 분리 GPU 비용 절반 이하 복잡한 태스크 품질 저하
    텐서 병렬화 소형 GPU 여러 장 활용 대형 GPU 구매 비용 절감 GPU 간 통신 오버헤드

    마무리: vLLM으로 LLM 운영, 이제 좀 해볼 만합니다

    처음에 LLM 추론 서버를 직접 운영한다고 했을 때 팀에서 반응이 냉담했어요. 비용이 너무 많이 든다고요. 근데 vLLM을 도입하고 양자화를 적용한 후로는 분위기가 달라졌습니다. 같은 GPU로 훨씬 많은 요청을 처리할 수 있게 됐거든요.

    정리하면 이렇습니다.

    • ✅ vLLM은 PagedAttention과 Continuous Batching으로 기존 대비 높은 처리량을 제공
    • ✅ OpenAI 호환 API로 기존 코드 재사용 가능 — 마이그레이션 비용 최소화
    • ✅ 양자화(AWQ/GPTQ)로 GPU 메모리 절약 → 더 작은 하드웨어에서 운영 가능
    • ✅ 텐서 병렬화로 대형 모델도 멀티 GPU에서 실행 가능
    • ✅ Prometheus 메트릭으로 성능 모니터링 가능

    처음 세팅할 때는 OOM이니 NCCL 오류니 삽질이 좀 있지만, 한 번 안정화되면 정말 편하게 쓸 수 있어요. 특히 자체 LLM 인프라를 구축하려는 분들께 강력 추천합니다.

    다음 글에서는 vLLM과 Kubernetes(쿠버네티스)를 연동해서 오토스케일링 추론 서버를 구성하는 방법을 다룰 예정이에요. GPU 노드를 자동으로 늘리고 줄이는 거라 비용 최적화에 더 도움이 될 거예요.

    혹시 설정하다가 막히는 부분이 있으시면 댓글로 남겨주세요. 아는 선에서 최대한 도와드리겠습니다! 🎉

    자주 묻는 질문 (FAQ)

    Q. vLLM은 무료인가요?

    네, vLLM은 Apache 2.0 라이선스의 오픈소스 프로젝트입니다. 소프트웨어 자체는 무료지만 실행할 GPU 하드웨어 비용은 별도로 발생합니다.

    Q. CPU만으로 vLLM을 실행할 수 있나요?

    vLLM은 기본적으로 NVIDIA GPU 환경에 최적화되어 있습니다. CPU 추론은 llama.cpp 같은 다른 도구가 더 적합할 수 있어요.

    Q. 어떤 모델이 vLLM에서 지원되나요?

    LLaMA, Mistral, Falcon, GPT-NeoX, OPT 등 주요 오픈소스 모델들을 지원합니다. 정확한 목록은 vLLM 공식 GitHub의 Supported Models 문서에서 확인하세요.

    Q. vLLM과 TGI(Text Generation Inference)의 차이는 뭔가요?

    TGI는 Hugging Face에서 만든 추론 서버이고, vLLM은 UC Berkeley에서 개발했습니다. 둘 다 고성능 LLM 추론을 목표로 하지만 내부 구현 방식이 다릅니다. 어떤 게 더 좋다기보다는 모델과 환경에 따라 성능 차이가 날 수 있으니 직접 테스트해보는 걸 권장합니다.

  • [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()로 병합할 수 있지만, 어댑터를 분리 유지하면 나중에 다른 어댑터로 교체하기 쉽습니다. 여러 도메인 모델을 관리한다면 분리 유지를 추천합니다.