[트러블 슈팅] 로컬 단일 GPU에서 멀티 에이전트 LLM 파이프라인 실행하기: 직렬화와 VRAM 관리

2026. 4. 22. 10:50·4. [팀] 프로젝트 및 공모전/4-5 AgentShield(보안 플랫폼)

이 글은 제 보안 프로젝트 AgentShield의 개인 트러블 슈팅 및 기록 블로그입니다.

배경

AgentShield는 LangGraph 기반 Red/Blue 에이전트 공방 파이프라인이다. Phase 1(공격 스캔) → Phase 2(변형 공격) → Phase 3(방어 코드) → Phase 4(방어 검증)로 이어지며, 각 단계에서 여러 LLM 에이전트가 Ollama를 호출한다.

로컬 GPU(4070Ti, 12GB VRAM)에서 이 파이프라인을 돌리자 다음 문제들이 동시에 터졌다:

Ollama returned empty response (attempt 1/3)
Ollama API Error 500: {"error":"EOF"}
[Target LLM] timeout (attempt 1/3)

에러 로그는 같은 초에 같은 요청이 여러 번 찍히고, 모델 로딩 충돌이 발생했다. 단일 GPU에서 여러 에이전트가 동시에 Ollama를 호출하면 어떤 일이 벌어지는가 — 이 글은 그 문제와 해결 과정을 다룬다.


문제 1: asyncio.gather가 만든 환상의 병렬성

증상

asyncio.gather로 12개 공격 패턴을 동시 실행하자 로그에 같은 타임스탬프에 2개씩 HTTP 요청이 찍혔다.

[TRACE] [18:00:13] http post -> http://localhost:8010/chat
[TRACE] [18:00:13] http post -> http://localhost:8010/chat   ← 동시 요청

asyncio.Semaphore(1)을 걸었음에도 효과가 없었다.

원인 분석

첫 번째 문제는 세마포어 위치였다. 세마포어가 _execute_attack_pattern 내부의 타겟 호출 부분만 감쌌는데, asyncio.gather는 12개 코루틴을 동시에 이벤트 루프에 올린다.

# 잘못된 구조: gather가 모든 태스크를 동시 스케줄
tasks = [_execute_attack_pattern(...) for pattern in patterns]
results = await asyncio.gather(*tasks)  # 12개 동시 실행

각 태스크 내부에서 세마포어를 획득하더라도, judge 호출(full_judge)은 세마포어 밖에 있었다. 결과적으로:

  • 패턴 A: 타겟 호출 완료 → 세마포어 해제 → judge 시작
  • 패턴 B: 세마포어 획득 → 타겟 호출 시작
  • 동시에 타겟(gemma4) + judge(qwen3.5) = 2개 모델 활성화

두 번째 문제는 이중 로깅이었다. _patched_phase2_target_for_ollama가 httpx.AsyncClient를 전역 교체(phase2_red_agent.httpx.AsyncClient = OllamaCompatibleAsyncClient)했기 때문에, Phase 1의 httpx.AsyncClient도 패치된 클라이언트가 되어 Phase 1 전용 ollama_send_fn과 OllamaCompatibleAsyncClient 양쪽에서 중복 로깅이 발생했다. 실제 요청은 1개였지만 로그는 2개가 찍혔다.

해결: gather 제거, 순차 for 루프로 완전 직렬화

# 수정: 물리적으로 1개씩 순차 처리
results = []
for i, pattern in enumerate(attack_patterns):
    try:
        result = await _execute_attack_pattern(...)
        results.append(result)
    except Exception as e:
        logger.error(f"[Phase1] Pattern {i + 1} unhandled exception: {e}")

세마포어 없이도 물리적으로 1개씩 처리된다. Semaphore를 통한 동시성 제어는 같은 이벤트 루프에서 코루틴이 경쟁하는 구조에서만 의미있다. 애초에 동시 실행하지 않으면 세마포어 경쟁 자체가 없다.


문제 2: 여러 모델 동시 로드 — VRAM 고갈

증상

ollama ps로 현재 로드된 모델을 확인했을 때:

qwen-coder:latest    21.9GB
qwen3.5:4b            5.8GB
gemma4:e2b            7.9GB

총 35.6GB — 12GB VRAM에서 불가능한 구성

원인: Docker 이미지에 구워진 구버전 .env

백엔드 Docker 컨테이너가 2일 전 빌드 시 .env가 이미지 내부에 복사되어 있었다. 이 구버전 .env에는:

OLLAMA_MODEL=gemma4:e2b
OLLAMA_RED_MODEL=gemma4-ara-abliterated

그리고 llm_client.py의 ollama_target_models에 하드코딩된 기본값:

self.ollama_target_models = {
    "red": os.getenv("OLLAMA_RED_TARGET_MODEL", "agentshield-red"),   # 존재하지 않음
    "judge": os.getenv("OLLAMA_JUDGE_TARGET_MODEL", "agentshield-judge"),  # 존재하지 않음
    ...
}

agentshield-red를 Ollama에 요청 → 404 응답 → fallback 로직:

if response.status_code == 404:
    fallback_model = role_config["ollama_model"]  # 구 .env의 값
    self.active_ollama_model = fallback_model

구 .env → OLLAMA_RED_MODEL=gemma4-ara-abliterated → 이것도 없으면 연쇄 fallback → qwen-coder:latest 로드.

Python의 load_dotenv() 함정

load_dotenv()는 기본적으로 이미 환경변수에 있는 값을 덮어쓰지 않는다(override=False). Docker 컨테이너는 docker-compose.yml의 environment 섹션 값이 먼저 주입되고, 이후 load_dotenv()가 이미지 내부 .env를 읽어도 덮어쓰지 않는다.

따라서 docker compose restart(컨테이너 재시작)로는 새 환경변수가 반영되지 않는다. docker compose up -d --force-recreate로 컨테이너를 재생성해야 한다.

해결: docker-compose에 env 명시 + 모델 단일화

# docker-compose.yml
services:
  backend:
    environment:
      OLLAMA_MODEL: qwen3.5:4b
      OLLAMA_RED_MODEL: qwen3.5:4b
      OLLAMA_JUDGE_MODEL: qwen3.5:4b
      OLLAMA_GUARD_MODEL: qwen3.5:4b
      OLLAMA_BLUE_MODEL: qwen3.5:4b
      OLLAMA_BASE_TARGET_MODEL: qwen3.5:4b
      OLLAMA_RED_TARGET_MODEL: qwen3.5:4b
      OLLAMA_JUDGE_TARGET_MODEL: qwen3.5:4b
      OLLAMA_BLUE_TARGET_MODEL: qwen3.5:4b

docker-compose.yml에 명시된 environment 값은 이미지 내부 .env보다 우선 적용된다. 재생성 후 확인:

docker exec agentshield-backend-1 python3 -c "
from backend.agents.llm_client import AgentShieldLLM
llm = AgentShieldLLM()
print(llm.active_ollama_model)          # → qwen3.5:4b
print(llm.ollama_target_models)         # → 전부 qwen3.5:4b
"

문제 3: LangGraph 내부 fan-out의 GPU 경쟁

구조

LangGraph judge graph:

scanner → strict_auditor ──┐
        → context_auditor ─┤→ consensus → [debate] → END

scanner에서 두 auditor로 fan-out이 발생한다. LangGraph의 ainvoke는 이를 asyncio로 동시 실행한다. 두 auditor 모두 call_ollama_judge()를 호출 — 같은 모델에 동시 요청 2개.

해결: 전역 Semaphore

_ollama_semaphore = asyncio.Semaphore(1)

async def call_ollama_judge(...):
    async with _ollama_semaphore:      # strict와 context가 여기서 직렬화
        async with aiohttp.ClientSession(...) as session:
            for attempt in range(max_retries):
                ...

핵심: 세마포어를 retry loop 전체에 씌워야 한다. retry 사이의 sleep도 세마포어를 잡고 있으면 다른 호출이 그 시간 동안 대기한다. 올바른 구조는 sleep을 세마포어 밖으로 빼는 것이다 — 단, 단일 GPU 단순화 후에는 Phase 1 자체가 순차 실행이므로 judge도 한 번에 1개만 실행된다.


최종 아키텍처: 단일 GPU 직렬화 원칙

[Phase 1 for loop]
  패턴 1 → 타겟 호출 → judge(strict → context, _ollama_semaphore(1))
  패턴 2 → 타겟 호출 → judge(strict → context, _ollama_semaphore(1))
  ...

동시 Ollama 호출 수: 최대 1개 (Phase 1 순차 실행 시)

모델 전환(gemma4 → qwen3.5) 없이 단일 모델(qwen3.5:4b)로 통일하면 모델 swap 오버헤드도 제거된다.


Docker + 로컬 GPU LLM 개발 체크리스트

# 1. 현재 로드된 Ollama 모델과 VRAM 사용량 확인
curl -s http://localhost:11434/api/ps | python3 -c "
import sys,json
for m in json.load(sys.stdin).get('models',[]):
    print(f'{m[\"name\"]}: {m[\"size\"]/1e9:.1f}GB')
"

# 2. 불필요한 모델 언로드 (keep_alive=0)
curl http://localhost:11434/api/generate -d '{"model":"unused-model","keep_alive":0}'

# 3. Docker 컨테이너 환경변수 실제 적용 확인
docker exec <container> env | grep OLLAMA

# 4. 환경변수 변경 후 재생성 (restart가 아닌 up --force-recreate)
docker compose up -d --force-recreate <service>

핵심 요약

문제 잘못된 접근 올바른 접근
동시 요청 제어 asyncio.Semaphore + gather gather 제거, 순차 for 루프
여러 모델 동시 로드 각 에이전트마다 다른 모델 전체 파이프라인 단일 모델 통일
Docker env 반영 docker restart docker compose up --force-recreate
LangGraph fan-out 제어 없음 전역 Semaphore(1)로 직렬화

 

로컬 단일 GPU에서 멀티 에이전트를 돌릴 때의 원칙: 동시성은 버리고 직렬화를 선택하라. GPU는 병렬 호출을 받아도 결국 큐에서 순차 처리한다. 코드 레벨에서 미리 직렬화하면 OOM, EOF, 타임아웃 없이 안정적으로 실행된다. 하드웨어가 많이 바쳐진다면 동시성도 좋다!

'4. [팀] 프로젝트 및 공모전 > 4-5 AgentShield(보안 플랫폼)' 카테고리의 다른 글

[AgentShield] 진행 상황 및 개인 공부 정리 (2): 파이프라인, DB 연결, 데이터 축적  (0) 2026.04.27
[AgentShield] 참고한 사이트 및 이유 총 정리 (1)  (0) 2026.04.26
[트러블슈팅] Ollama EOF 에러의 원인: Thinking 모델과 KV Cache 오버플로  (0) 2026.04.21
[개인 공부] AgentShield: 자동화 모의해킹 파이프라인 및 DPO 데이터 수집 아키텍처 정리  (0) 2026.04.15
[개인 공부 메모] AgentShield LLM Judge 오탐 문제 분석 및 보안 판정 동향  (0) 2026.04.14
'4. [팀] 프로젝트 및 공모전/4-5 AgentShield(보안 플랫폼)' 카테고리의 다른 글
  • [AgentShield] 진행 상황 및 개인 공부 정리 (2): 파이프라인, DB 연결, 데이터 축적
  • [AgentShield] 참고한 사이트 및 이유 총 정리 (1)
  • [트러블슈팅] Ollama EOF 에러의 원인: Thinking 모델과 KV Cache 오버플로
  • [개인 공부] AgentShield: 자동화 모의해킹 파이프라인 및 DPO 데이터 수집 아키텍처 정리
고니3000원
고니3000원
프로젝트의 구현 과정과 기술적 노하우를 담았습니다. AI 모델 연구와 매일의 학습 기록을 차곡차곡 공유하고 있습니다. [ 매너 & 태도 ] * 항상 겸손해라.
늘 자신을 낮추고 겸손함을 잃지 마라.
 * 나이 불문 예의를 지켜라.
나이와 지위를 막론하고, 누구에게나 변함없는 예의를 갖추어라.
 * 행복하겠다는 생각을 버려야 행복하다.
행복에 대한 강박과 집착을 내려놓을 때, 비로소 진정한 행복이 찾아온다.
 [ 리더십 & 실행 ] * 통찰력, 결단력,
  • 고니3000원
    곤이의 공부 블로그
    고니3000원
  • 전체
    오늘
    어제
    • 분류 전체보기 (212) N
      • 1. AI 논문 + 모델 분석 (21)
        • AI 논문 분석 (13)
        • AI 모델 분석 (8)
      • 2. 자료구조와 알고리즘 (16)
        • 2-1 자료구조와 알고리즘 (13)
        • 2-2 강화학습 알고리즘 (3)
      • 3. 자습 & 메모(실전, 실습, 프로젝트) (27)
        • 3-1 문제 해석 (4)
        • 3-2 메모(실전, 프로젝트) (14)
        • 3-3 배포 실전 공부 (7)
        • 3-4 최신 기술 분석 (2)
      • 4. [팀] 프로젝트 및 공모전 (31)
        • 4-1 팀 프로젝트(메모, 공부) (1)
        • 4-2 Meat-A-Eye (6)
        • 4-3 RL-Tycoon-Agent (3)
        • 4-4 구조물 안정성 물리 추론 AI 경진대회(D.. (4)
        • 4-5 AgentShield(보안 플랫폼) (17)
      • 5. [개인] 프로젝트 및 공모전 (21) N
        • 4-1 귀멸의칼날디펜스(자바스크립트 활용) (5)
        • 4-2 바탕화면 AI 펫 프로그램 (4)
        • 4-3 개인 프로젝트(기타) (3)
        • 4-4 공모전 (5) N
        • 4-5 나만의 로컬 LLM 멀티 에이전트 구축 (4)
      • 개념 정리 step1 (32)
        • Python 기초 (7)
        • DBMS (1)
        • HTML | CSS (3)
        • Git | GitHub (1)
        • JavaScript (5)
        • Node.js (5)
        • React (1)
        • 데이터 분석 (6)
        • Python Engineering (3)
      • 개념 정리 step2 (60)
        • Machine | Deep Learning (15)
        • 멀티모달(Multi-modal) (23)
        • 강화 학습 (10)
        • AI Agent (9)
        • 메디컬 이미지 (3)
      • 개인 공부 - 내가 공부하고 싶은 모든 것 (2)
        • 1. 인프라 (2)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

    • Notion-포트폴리오
    • Github
  • 공지사항

    • ‘박영곤’ 나의 핵심 가치
  • 인기 글

  • 태그

    ViT
    pandas
    Agent
    파인튜닝
    llm
    Python
    보안
    Ollama
    API
    paddleocr
    데이터분석
    논문 리뷰
    학습
    프로젝트
    RAG
    전처리
    transformer
    자바스크립트
    자료구조
    Ai
    공모전
    인공지능
    강화학습
    Lora
    알고리즘
    OCR
    github
    Vision
    파이썬
    구현
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.5
고니3000원
[트러블 슈팅] 로컬 단일 GPU에서 멀티 에이전트 LLM 파이프라인 실행하기: 직렬화와 VRAM 관리
상단으로

티스토리툴바