Skip to content

Latest commit

 

History

History
403 lines (304 loc) · 18.4 KB

File metadata and controls

403 lines (304 loc) · 18.4 KB

로컬 GPU 머신에서 LLM 운용하기 — Agentic AI 비용 절감 가이드

배경

Claude, GPT-4 같은 구독형 클라우드 API는 토큰 비용이 누적될 수 있습니다.
Agentic AI 워크플로우는 단일 실행 당 수천 ~ 수만 토큰을 소비하기 때문에 운영 비용이 빠르게 증가합니다.
로컬 GPU 머신에 오픈소스 모델을 직접 올리면 추론 비용 = 전기세 + 하드웨어 감가만 남습니다.


현재 적용 환경 (검증 완료)

항목
GPU NVIDIA TITAN V × 4 (VRAM 12 GB × 4 = 48 GB)
CUDA 12.2 / Compute 7.0 (Volta)
서빙 스택 Ollama v0.24.0
모델 Qwen3 30B A3B Q4_K_M (qwen3:30b-a3b-q4_K_M, ~18 GB) ← 현재 사용 중
GPU 분산 OLLAMA_SCHED_SPREAD=true — GPU 0-3 균등 분산
컨텍스트 길이 8,192 토큰 (OLLAMA_CONTEXT_LENGTH=8192 — KV cache GPU fit 필수)
Flight API SerpAPI (실제 Google Flights 데이터)

TITAN V (Volta CC 7.0) 주의: bfloat16 / Flash Attention 2 하드웨어 미지원.
Ollama는 float16으로 자동 폴백하므로 정상 동작합니다.


1. 로컬 GPU 머신에 모델 구축

1-1. 추천 모델 (2026년 5월 기준)

Q4 = 4-bit 양자화. VRAM이 부족하면 Q3/Q2도 가능하나 품질이 저하됩니다.
MoE = Mixture of Experts. 전체 파라미터 중 일부(active)만 추론에 사용 → VRAM 효율 높음.
tool-call 열은 bind_tools / with_structured_output 호환 여부입니다.

① 고성능 — VRAM 48 GB 이상 (멀티 GPU 또는 A100/H100)

모델 구조 최소 VRAM tool-call 특징
Qwen3-235B-A22B MoE (22B active) 132 GB (Q4) Alibaba, 프론티어급 품질, Apache 2.0
Kimi K2.6 MoE (32B active) 80 GB (Q2) Moonshot AI, 에이전트 스웜(300 sub-agents), 코딩 최강
Llama-4-Maverick MoE 80 GB+ Meta, 멀티모달, 대형 에이전트 워크플로우
Gemma 4 26B A4B Q8_0 MoE (4B active) 28 GB (Q8) 초기 검증 모델 — 속도 느림, Qwen3로 교체

② 중간 — VRAM 2048 GB (RTX 4090 12장, A40)

모델 구조 최소 VRAM tool-call 특징
Qwen3-30B-A3B Q4_K_M MoE (3B active) 18 GB (Q4) 현재 사용 중 — TITAN V 4장 검증. 플래너 ~63s
Qwen3-32B Dense 19 GB (Q4) Qwen3 최대 Dense, 범용 최우수 가성비
Qwen3.6-35B-A3B MoE (3.5B active) 12 GB (Q4) VRAM 대비 품질 효율 탁월
Nemotron-Cascade-2-30B Dense 22 GB (Q4) NVIDIA, ~54 tok/s (소비자 GPU), GPT-4o mini급
Devstral-Small-2-24B Dense 18 GB (Q4) Mistral, 에이전트 코딩 특화, SWE-bench 상위
Mistral-Large-3 Dense 32 GB (Q4) Mistral 최신, 함수 호출 안정성 우수
Gemma-4-27B Dense 20 GB (Q4) Google 최신, 멀티모달, 긴 컨텍스트

③ 경량 — VRAM 8~16 GB (RTX 3080/4070, Mac M 시리즈)

모델 구조 최소 VRAM tool-call 특징
Qwen3-8B Q4_K_M Dense 5.2 GB (Q4) 검증 완료 — 단일 GPU fit, 플래너 ~35s, 빠른 테스트용
Llama-4-Scout-17B MoE 10 GB (Q4) Meta MoE, 2026년 5월 기준 경량 전체 1위
Qwen3-14B Dense 10 GB (Q4) 소형 중 tool-call 안정성 최상
Gemma-3-12B-Instruct Dense 9 GB (Q4) 경량 대비 품질 우수
Phi-4-reasoning-plus Dense 12 GB (Q4) Microsoft, reasoning 특화
Qwen3-7B Dense 6 GB (Q4) 초경량, CPU fallback 실용적
Mistral-Small-3.2-22B Dense 16 GB (Q4) 빠른 추론, 경량 최강 함수 호출

④ Agentic AI 권장 선택 (이 프로젝트 기준)

GPU 환경 권장 모델 Ollama 모델명 플래너 속도
TITAN V × 4 (48 GB) Qwen3-30B-A3B Q4_K_M ✅ 검증 완료 qwen3:30b-a3b-q4_K_M ~63s
TITAN V × 4 (빠른 테스트) Qwen3-8B Q4_K_M ✅ 검증 완료 qwen3:8b ~35s
48 GB 이상 (RTX 3090 × 2 등) Qwen3-32B qwen3:32b
24 GB (RTX 4090 1장) Qwen3-32B (Q4, 19GB) qwen3:32b
20~24 GB Nemotron-Cascade-2-30B 또는 Devstral-Small-2-24B nemotron-cascade2:30b
16 GB Mistral-Small-3.2 또는 Qwen3.6-35B-A3B qwen3.6:35b-a3b
8~12 GB Llama-4-Scout-17B 또는 Qwen3-14B llama4:scout

Gemma 4 26B A4B Q8_0 (gemma4:26b-a4b-it-q8_0, 28 GB): 초기 검증 모델. PCIe overhead + Q8_0 양자화 조합으로 4-GPU 분산 시 속도가 느림 (~수 분/호출). Qwen3 계열로 교체됨.

1-2. 서빙 스택 선택

방법 A — Ollama v0.24.0 (검증된 방법)

v0.24.0부터 배포 포맷이 .tgz.tar.zst로 변경되었습니다.
설치 스크립트(scripts/setup_ollama_gemma.sh)를 사용하거나 아래 절차를 따르세요.

# 의존성
sudo apt-get install -y zstd

# GitHub Releases에서 직접 다운로드 (공식 install.sh는 환경에 따라 실패할 수 있음)
wget https://github.com/ollama/ollama/releases/download/v0.24.0/ollama-linux-amd64.tar.zst \
     -O /tmp/ollama.tar.zst
zstd -d /tmp/ollama.tar.zst -o /tmp/ollama.tar

# 전체 tar 압축 해제 (CUDA 라이브러리 포함 — 이 단계가 GPU 가속의 핵심)
mkdir -p ~/.local
tar -xf /tmp/ollama.tar -C ~/.local/

# PATH 추가
export PATH="$HOME/.local/bin:$PATH"
echo 'export PATH="$HOME/.local/bin:$PATH"' >> ~/.bashrc

# 모델 다운로드
ollama pull qwen3:30b-a3b-q4_K_M

# Ollama 실행 — CONTEXT_LENGTH(GPU fit) + SCHED_SPREAD(4-GPU 균등 분산) 필수
OLLAMA_CONTEXT_LENGTH=8192 OLLAMA_SCHED_SPREAD=true OLLAMA_NUM_PARALLEL=1 ollama serve &

CUDA 라이브러리 누락 주의: bin/ollama만 추출하면 GPU가 감지되어도
offloaded 0/31 layers to GPU 상태로 CPU 모드로 폴백됩니다.
반드시 전체 tar를 ~/.local/에 추출해야 lib/ollama/cuda_v12/libggml-cuda.so가 배포됩니다.

OLLAMA_CONTEXT_LENGTH 설정이 필요한 이유:

컨텍스트 길이 KV Cache 크기 모델+Cache 합계 GPU 결과
262,144 (기본) 매우 큼 48 GB 초과 ❌ CPU 폴백
8,192 (Q8_0) 작음 ~31 GB ✅ GPU 100% (Q8_0 기준)
16,384 (Q4_K_M) 작음 ~21 GB GPU 100% (권장)
# GPU 로드 확인
ollama ps
# NAME                      ID              SIZE     PROCESSOR    CONTEXT
# qwen3:30b-a3b-q4_K_M      a1b2c3d4e5f6    21 GB    100% GPU     8192

GPU 메모리 자동 언로드 (keep_alive):

Ollama는 마지막 요청 후 일정 시간이 지나면 모델을 GPU에서 자동으로 내립니다.

상황 언로드 시점
기본값 마지막 요청 후 5분 뒤 자동 언로드
keep_alive 지정 해당 시간 후 언로드
keep_alive: -1 프로세스 종료 전까지 영구 유지
keep_alive: 0 요청 완료 즉시 언로드

에이전트 실행 중에는 요청이 계속 들어오므로 모델이 유지되고, 마지막 요청(ReflectionAgent) 완료 후 5분이 지나면 자동으로 내려갑니다. 다음 실행 시 재로드에 약 30초가 소요됩니다.

# 모델 사전 로드 + 유지 시간 지정 (에이전트 실행 전 권장)
curl -s http://localhost:11434/api/generate \
  -d '{"model":"qwen3:30b-a3b-q4_K_M","prompt":"hi","stream":false,"keep_alive":"10m"}' > /dev/null

# 항상 올려두려면 Ollama 서버 실행 시 환경 변수 추가
OLLAMA_KEEP_ALIVE=-1 OLLAMA_CONTEXT_LENGTH=8192 OLLAMA_SCHED_SPREAD=true ollama serve &

방법 B — vLLM (프로덕션 수준 처리량)

pip install vllm

python -m vllm.entrypoints.openai.api_server \
  --model google/gemma-4-27b-it \
  --quantization awq \
  --tensor-parallel-size 4 \
  --max-model-len 8192 \
  --port 8000
  • OpenAI 호환 엔드포인트: http://localhost:8000/v1
  • 배치 처리·연속 배칭 지원 → 동시 요청에 강함

방법 C — llama.cpp / llama-server (저사양 GPU · CPU)

git clone https://github.com/ggerganov/llama.cpp && cd llama.cpp
cmake -B build -DLLAMA_CUDA=ON && cmake --build build -j

./build/bin/llama-server \
  -m ./models/gemma-4-26b-a4b-it-q8_0.gguf \
  --port 8080 \
  --n-gpu-layers 31
  • OpenAI 호환 엔드포인트: http://localhost:8080/v1

2. Flight Monitor Agent 와 연동

2-1. 환경 변수 설정 (.env)

# 로컬 LLM 전환
LLM_PROVIDER=local
LOCAL_LLM_BASE_URL=http://localhost:11434/v1   # Ollama
LOCAL_LLM_MODEL=qwen3:30b-a3b-q4_K_M          # 또는 qwen3:8b (빠른 테스트)
LOCAL_LLM_API_KEY=ollama

# Ollama 서버 실행 시 필수 환경 변수
OLLAMA_CONTEXT_LENGTH=8192   # GPU fit — 기본값(262144)에서는 CPU 폴백 발생
OLLAMA_SCHED_SPREAD=true     # 멀티 GPU 균등 분산 (없으면 GPU 3대에만 편중)

# 클라우드 LLM으로 복구 시 (agents/ 코드 변경 불필요 — LLM_PROVIDER만 전환)
# LLM_PROVIDER=anthropic
# ANTHROPIC_API_KEY=sk-ant-...
# LLM_MODEL=claude-haiku-4-5-20251001

2-2. config.py 구조

이미 LLM_PROVIDER 기반 스위칭이 구현되어 있습니다:

LLM_PROVIDER: str = os.getenv("LLM_PROVIDER", "anthropic")
LOCAL_LLM_BASE_URL: str = os.getenv("LOCAL_LLM_BASE_URL", "http://localhost:11434/v1")
LOCAL_LLM_MODEL: str = os.getenv("LOCAL_LLM_MODEL", "qwen2.5:72b-instruct-q4_K_M")
LOCAL_LLM_API_KEY: str = os.getenv("LOCAL_LLM_API_KEY", "ollama")

2-3. agents/ — LLM 팩토리 및 에이전트별 thinking 전략

Gemma 4 thinking 모드 이슈

Gemma 4는 thinking 모드가 활성화되면 {"thought": "..."} 형태의 JSON을 먼저 출력합니다. with_structured_output 이 이를 Pydantic 스키마로 파싱하려 하면 필드 불일치로 ValidationError가 발생합니다.

해결 방법: 에이전트 목적에 따라 thinking 전략을 분리합니다.

에이전트 팩토리 thinking 이유
SearchAgent make_llm False tool call 정밀도 우선
PriceAnalysisAgent make_thinking_llm 2단계 히스토리·전략 메모·RAG 종합 판단
NotificationAgent Python 직접 실행 없음 (LLM 미사용) 이메일 발송·메모리 기록은 판단 불필요 — LLM 제거로 6분+ 대기 문제 해결
ReflectionAgent make_thinking_llm 2단계 트렌드 분석·전략 수립
PlannerAgent make_thinking_llm 2단계 로컬 LLM(Gemma) None 반환 방지 — 추론 후 안정적 structured output
ProactivityAgent make_thinking_llm 2단계 복합 컨텍스트 판단

구현

class TwoStepLLM:
    """local 전용: think=True 자유 텍스트 추론 → think=False structured_output 포맷팅."""
    def invoke(self, messages):
        reasoning = self._reasoning_llm.invoke(messages)          # 1단계: 자유 추론
        format_messages = list(messages) + [
            AIMessage(content=reasoning.content or ""),
            HumanMessage(content="Based on your analysis above, fill in the structured output fields exactly."),
        ]
        return self._format_llm.invoke(format_messages)           # 2단계: 구조화 포맷


def make_llm(tools=None, structured_output=None, thinking=False):
    """tool call / structured_output 에이전트용 — thinking=False 고정."""
    if config.LLM_PROVIDER == "local":
        llm = ChatOpenAI(..., model_kwargs={"extra_body": {"options": {"think": thinking}}})
    else:
        llm = ChatAnthropic(model=config.LLM_MODEL, temperature=0)
    ...


def make_thinking_llm(structured_output):
    """추론이 필요한 에이전트용 — provider-agnostic 2단계 호출.

    local  : think=True 추론(1단계) → think=False structured_output 포맷(2단계)
    claude : 내장 추론으로 직접 structured_output (코드 변경 불필요)
    """
    if config.LLM_PROVIDER == "local":
        return TwoStepLLM(
            reasoning_llm=_make_local_llm(thinking=True),
            format_llm=_make_local_llm(thinking=False).with_structured_output(structured_output),
        )
    else:
        return ChatAnthropic(model=config.LLM_MODEL, temperature=0).with_structured_output(structured_output)

extra_body 사용 이유: model_kwargs={"options": ...} 는 OpenAI Python 클라이언트가 알 수 없는 파라미터로 거부합니다. extra_body를 쓰면 클라이언트가 검증 없이 JSON body에 병합하여 Ollama로 전달합니다.

cloud 전환 시 코드 변경 불필요: LLM_PROVIDER=anthropic으로 바꾸면 make_thinking_llm이 Claude 직접 호출로 자동 전환됩니다. Claude는 내부 추론이 내장되어 있어 2단계 분리가 필요 없습니다.

각 파일에서의 import:

# agents/planner.py — 2단계 thinking (로컬 LLM None 반환 방지, 안정적 structured output)
from core.llm import invoke_with_retry, make_thinking_llm
llm = make_thinking_llm(SearchPlan)

# agents/proactivity.py — thinking 유지 (복합 컨텍스트 판단)
from core.llm import make_thinking_llm
llm = make_thinking_llm(ProactivityAnalysis)

2-4. 실행

# Ollama 서버 시작 (CONTEXT_LENGTH + SCHED_SPREAD 필수)
OLLAMA_CONTEXT_LENGTH=8192 OLLAMA_SCHED_SPREAD=true OLLAMA_NUM_PARALLEL=1 ollama serve &

# (선택) 모델 사전 로드 — 첫 에이전트 호출 지연 방지
curl -s http://localhost:11434/api/generate \
  -d '{"model":"qwen3:30b-a3b-q4_K_M","prompt":"hi","stream":false,"keep_alive":"10m"}' > /dev/null

# Goal Decomposition 모드 — 자연어 목표를 자동으로 검색 계획으로 분해 (SerpAPI)
python -m examples.flight_monitor.run \
  --mode serpapi \
  --goal "ICN에서 동남아 30만원 이하 최저가를 2026년 7월~2027년 1월 사이에서 찾아줘" \
  --max-price 300000 --currency KRW --user <이름>

# 기본 단일 노선 모니터링 모드 (mock)
python -m examples.flight_monitor.run \
  --origin ICN --dest NRT --date 2026-07-15 --max-price 250000

3. Tool Call 호환성 주의사항

LangChain의 bind_tools / with_structured_output은 모델이 tool call 형식을 지원해야 동작합니다.

기능 Ollama vLLM 권장 모델
bind_tools instruct-tuned 모델 필수 대부분 지원 qwen3:30b-a3b-q4_K_M
with_structured_output 지원 (JSON mode) 지원 qwen3:30b-a3b-q4_K_M
멀티 GPU 레이어 분산 자동 --tensor-parallel-size 설정 Ollama 자동 ✓

Qwen3 계열 및 Gemma 4 -it 태그 모델 모두 tool call을 지원합니다.
non-instruct 모델(-it 태그 없음)은 tool call을 지원하지 않습니다.


4. 예상 비용 비교

구분 비용
Claude Sonnet (API) 입력 $3 / 출력 $15 per 1M 토큰
로컬 TITAN V × 4 (전기세) 약 $0.040.08 / 시간 (전력 300600W 기준)
로컬 RTX 4090 (전기세) 약 $0.05~0.10 / 시간 (전력 450W 기준)

하루 수백만 토큰을 처리하는 워크플로우라면 수주 내 하드웨어 비용 회수가 가능합니다.


5. 빠른 시작 체크리스트

  • GPU 드라이버 및 CUDA 설치 확인 (nvidia-smi) — TITAN V × 4 (CC 7.0) 확인
  • Ollama v0.24.0 전체 tar 추출 (CUDA 라이브러리 포함)
  • qwen3:30b-a3b-q4_K_M 모델 다운로드 (~18 GB)
  • OLLAMA_CONTEXT_LENGTH=8192 OLLAMA_SCHED_SPREAD=true로 Ollama 실행 → GPU 4개 균등 분산 확인
  • .envLLM_PROVIDER=local, LOCAL_LLM_MODEL=qwen3:30b-a3b-q4_K_M 설정
  • make_llm / make_thinking_llm / TwoStepLLM 팩토리 구현 — 에이전트별 thinking 전략 분리
  • agents/planner.pymake_thinking_llm(SearchPlan) (로컬 LLM None 반환 방지)
  • notification_node — Python 직접 실행으로 전환, LLM 제거 (6분+ 타임아웃 문제 해결)
  • core/llm.py — 로컬 LLM timeout=300 설정
  • agents/proactivity.pymake_thinking_llm 유지 (복합 컨텍스트 판단)
  • model_kwargs optionsextra_body 전환 (OpenAI 클라이언트 호환)
  • Goal Decomposition: 목표가 이하 옵션 최대 20개 수집 → 통합 이메일 발송
  • SerpAPI 실제 항공 데이터로 Goal Decomposition 모드 end-to-end 검증
  • 결과 이메일 발송 확인 — 옵션 카드 반복 포맷 (━ 구분선)

검증 결과 (2026-05-22 ~ 2026-05-24):

[2026-05-22] 모델: Gemma 4 26B A4B Q8_0
탐색 노선: ICN → FUK/KIX/NRT/PVG (일본·중국 4개 도시)
예산: 300,000 KRW

탐색 결과 (가격순):
  1. ICN→FUK  2026-06-09  102,600 KRW  T'Way Air TW 207   ← 최저가 ✅
  2. ICN→FUK  2026-06-30  102,600 KRW  T'Way Air TW 205   ✅
  3. ICN→KIX  2026-06-16  117,600 KRW  T'Way Air           ✅
  4. ICN→NRT  2026-07-07  138,300 KRW  ZIPAIR Tokyo ZG 46  ✅
  5. ICN→PVG  2026-06-23  286,000 KRW  Shandong SC 4620    ✅

이메일: 🎉 목표가 이하 항공권 5개 발견! 최저 102,600 KRW → (수신자 이메일)
        (5개 옵션 카드 + 노선별 예약 딥링크 포함)
PlannerAgent: ~2-3분 (Gemma 4, 4-GPU PCIe overhead)

[2026-05-23] 모델: Gemma 4 26B A4B Q8_0
탐색 노선: ICN → TAO (칭다오) 왕복, 오전 10시~오후 6시 출발
예산: 400,000 KRW  /  --ticket-type round-trip (귀환일 PlannerAgent 자동 생성)

탐색 결과: LLM이 자율 결정 (상한 없음), 12개 목표가 이하
  최저가: ICN→TAO  2026-11-18 → 2026-11-25  327,500 KRW  Shandong SC 4610
이메일: 🎉 목표가 이하 항공권 12개 발견! 최저 327,500 KRW (즉시 발송, 4초)
NotificationAgent: LLM 제거 → Python 직접 실행 (이전 6분+ 타임아웃 해결)

[2026-05-24] 모델: Qwen3 8B Q4_K_M (속도 비교용)
탐색 노선: ICN → HRB (하얼빈) 왕복
예산: 400,000 KRW  /  waypoints 경유지 파싱 검증

PlannerAgent (5 타겟): ~35초  ← Gemma 4 대비 4~6배 빠름
경로 표시: ICN → TAO → HRB (Shandong 청도 경유 자동 파싱)
목표가 이하 없음 (최저가 474,400 KRW, 2026-07-05)

[2026-05-24] 모델: Qwen3 30B A3B Q4_K_M ← 현재 사용 중
탐색 노선: ICN → HRB (하얼빈) 왕복
예산: 400,000 KRW

PlannerAgent (5 타겟): ~63초
재계획 포함 전체: ~4분 30초
최저가: 474,400 KRW (2026-07-05, ICN → TAO → HRB, Shandong SC 4612)