본문으로 건너뛰기

© 2026 Molayo

r/LocalLLaMA분석2026. 06. 20. 14:25

vLLM Prefix Caching 및 OpenCode 병렬 에이전트를 사용한 Dual RTX 3090 환경에서의 실사용 속도 향상 방법

요약

Dual RTX 3090 환경에서 vLLM Prefix Caching과 OpenCode 병렬 에이전트를 활용해 개발 속도를 2.4배 향상시킨 최적화 사례를 다룹니다. 대규모 컨텍스트 사용 시 발생하는 KV 캐시 부족 문제를 비대칭 컨텍스트 프로필로 해결하는 기술적 방법을 제시합니다.

핵심 포인트

  • vLLM Prefix Caching과 병렬 에이전트 결합으로 개발 처리량 2.43배 향상
  • 대규모 컨텍스트 요청 시 발생하는 KV 캐시 스래싱 및 지연 시간 문제 분석
  • 비대칭 클라이언트 컨텍스트 프로필을 통한 GPU 메모리 관리 최적화
  • Dual RTX 3090 환경에서의 Tensor Parallelism 및 GPTQ 양자화 활용

🚀 vLLM Prefix Caching 및 OpenCode 병렬 에이전트를 사용하여 Dual RTX 3090 환경에서 실사용 속도를 2.4배 향상시킨 방법
안녕하세요, r/LocalLlama 여러분!
저는 dual RTX 3090 시스템을 사용하여 로컬 개발 에이전트 환경을 최적화한 과정을 깊이 있게 공유하고자 합니다. 우리 중 많은 사람이 vLLM의 합성 벤치마크(synthetic benchmarks)를 보고—로그에서 1,000+ 토큰/초의 프리필 처리량(prefill throughput)을 보는 것만으로도—'와, 정말 빠르다!'라고 생각합니다. 하지만 실제로 에이전트 오케스트레이터(예: OpenCode/Kilo Code)를 사용하여 코드를 작성하고, 테스트하고, 실행할 때의 실제 개발자 벽시계 대기 시간은 종종 매우 다른 이야기를 들려줍니다.
vLLM Prefix Caching, Asymmetric Client Context Windows, Parallel Tool Calling, 그리고 Contract-Driven Development (CDD)를 결합함으로써, 저는 순차적인 코드 생성에서 5개의 코딩 서브 에이전트(coding subagents)를 동시에 실행하는 수준으로 확장하여 대기 시간을 단축하고 실제 개발자 처리량(developer throughput)을 2.43배 향상시킬 수 있었습니다.
여기에 하드웨어, 구성, 기본 이론 및 실사용 벤치마크 데이터에 대한 정확한 기술적 분석이 있습니다.

  1. 하드웨어 및 엔진 설정

리그: Dual NVIDIA RTX 3090 (각 24GB VRAM), Tensor Parallelism 모드(TP=2)로 실행.
모델: llmfan46/Qwen3.6-27B-uncensored-heretic-v2-Native-MTP-Preserved-GPTQ-Int4 (thinking blocks를 지원하기 위해 qwen3.6-27b로 서비스됨).
양자화(Quantization): GPTQ Int4.
컨텍스트 제한: 256K 토큰 (--max-model-len 262144 네이티브 모델 지원).
추론 백엔드(Inference Backend): 환경 변수를 통해 FlashInfer 활성화:

  1. 기준선 및 메모리 고갈 위험성
    처음에 저희는 표준 구성으로 vLLM을 설정했습니다: --max-num-seqs 5 (이는 최대 병렬 요청 제한이었습니다). 개발 에이전트가 서브 에이전트를 순차적으로 트리거했을 때, 개발자의 경험은 느렸습니다.

더 심각한 문제는, 모델이 256K 컨텍스트 제한 (context limit)을 지원할 때 발생합니다. 만약 메인 에이전트 코디네이터 (coordinator)가 큰 컨텍스트(예: 200K 토큰)를 사용하고, 256K 제한을 상속받거나 요청하는 4~5개의 병렬 코딩 서브 에이전트 (subagents)를 생성하면, 스케줄러 (scheduler)는 즉시 GPU의 KV 캐시 (KV cache) 물리 용량을 초과하게 됩니다. GPTQ를 사용하는 24GB 카드에서 우리의 물리적 KV 캐시 용량은 약 691K 토큰입니다 (FP8 KV 캐시 사용 시). $$\text{Demand} = 200\text{K (Primary)} + (4 \times 256\text{K}) = 1,224\text{K tokens} > 691\text{K tokens}$$ 이는 대규모 컨텍스트 축출 (context eviction, 캐시 스래싱 (cache thrashing))을 유발하며, vLLM이 컨텍스트 블록을 처음부터 다시 계산하도록 강제함에 따라 거대한 프리필 지연 시간 (prefill latency) 스파이크를 일으킵니다.

  1. 해결책: 비대칭 클라이언트 컨텍스트 프로필 (Asymmetric Client Context Profiles)
    동시 실행되는 서브 에이전트들로 인해 GPU가 과부하되는 상황을 방지하면서, 코디네이터 에이전트가 256K 컨텍스트 제한을 활용할 수 있도록 OpenCode (opencode.jsonc)에서 비대칭 컨텍스트 프로필 (Asymmetric Context Profiles)을 구성했습니다.
    모든 에이전트 요청을 동일하게 처리하는 대신, 동일한 vLLM 백엔드를 가리키는 세 가지 별도의 API 프로필을 등록하여 컨텍스트 윈도우 (contextWindow)와 생성 제한 (maxTokens)을 나누었습니다:

{ "provider": { // 1. Primary Channel: 전체적인 계획을 위한 넓은 컨텍스트 "llm_local_primary": { "options": { "baseURL": "http://localhost:8000/v1" }, "models": { "qwen3.6-27b": { "contextWindow": 200000, // 대형 200K 윈도우 "maxTokens": 8192 } } }, // 2. Coder Channel: 로컬의 집중된 파일들 "llm_local_coder": { "options": { "baseURL": "http://localhost:8000/v1" }, "models": { "qwen3.6-27b": { "contextWindow": 65536, // 64K 컨텍스트로 제한 "maxTokens": 4096 } } }, // 3.

Utility 채널: 빠른 동작, 요약, 웹 검색
"llm_local_utility": { "options": { "baseURL": "http://localhost:8000/v1" }, "models": { "qwen3.6-27b": { "contextWindow": 16384, // 16K 컨텍스트만 사용 "maxTokens": 1024 } } } }, "agent": { "plan": { "model": "llm_local_primary/qwen3.6-27b" }, "build": { "model": "llm_local_primary/qwen3.6-27b" }, "general": { "model": "llm_local_coder/qwen3.6-27b" }, "explore": { "model": "llm_local_coder/qwen3.6-27b" }, "summary": { "model": "llm_local_utility/qwen3.6-27b" } } }

비대칭 프로필의 수학적 배경 (The Mathematics Behind Asymmetric Profiles)

주요 코디네이터(primary coordinator)가 200K 토큰에서 실행되고 2개의 병렬 개발 서브 에이전트(Coder)와 2개의 유틸리티 서브 에이전트(Utility)를 실행할 경우, 최악의 경우 할당량은 다음과 같습니다:

고정 생성 버퍼 예약 (Fixed Generation Buffer Reservation): $$ ext{Reserva} = (1 imes 8192) + (2 imes 4096) + (2 imes 1024) = 18,432 ext{ 토큰.}$$
물리적 KV 캐시 사용 가능 공간 (Physical KV Cache Space Remaining): $$ ext{KV Cache Disponible} = 691,176 - 18,432 = 672,744 ext{ 토큰.}$$
최악의 경우 컨텍스트 창 소비량 (Worst-Case Context Window Consumption) (캐시 미적중 시): $$ ext{Consumo} = 200,000 ext{ (Primary)} + (2 imes 65,536) + (2 imes 16,384) = 363,840 ext{ 토큰.}$$
GPU에 남은 안전 여유 공간 (Safety Margin remaining on the GPU): $$ ext{Caché Libre} = 672,744 - 363,840 = 308,904 ext{ 토큰.}$$

이러한 제한을 설정함으로써, 최대 동시 처리 용량으로 실행할 때도 GPU가 메모리가 부족하거나 스래싱(thrashing) 현상을 겪지 않도록 보장했습니다.

4. Prefix Caching 및 동시성을 위한 프롬프트 엔지니어링 (Prompt Engineering for Prefix Caching & Concurrency)

안전한 메모리 경계를 확보하는 것은 절반에 불과합니다. 우리는 또한 vLLM의 Prefix Caching을 최대화하고 병렬 생성을 강제해야 했습니다.

규칙 1: 불변 접두사 원칙 (The Principle of the Immutable Prefix)

vLLM은 프롬프트를 16 토큰 블록 단위로 왼쪽에서 오른쪽으로 처리합니다. 만약 프롬프트의 시작 부분(예: 동적 타임스탬프 삽입 또는 시스템 명령어 변경)에 단 하나의 토큰이라도 변경되면, 전체 다운스트림 캐시가 무효화됩니다.

우리의 구조: 시스템 프롬프트 (정적, Static) ➔ 프로젝트 사양/파일 (정적, Static) ➔ 대화 기록 (선형적으로 증가, Grows Linearly) ➔ 새로운 프롬프트 (동적, Dynamic, 항상 맨 마지막에 위치).
이 구조를 통해 연속적인 대화 과정에서 90% 이상의 Prefix Cache Hit Rate (프리픽스 캐시 적중률)를 일관되게 달성할 수 있었습니다.

규칙 2: 병렬 도구 호출 (Parallel Tool Calling)
일반적으로 LLM은 순차적으로 작성합니다. 하나의 도구를 호출하고, 다음 턴에서 결과를 기다린 다음, 두 번째 도구를 호출하는 방식입니다. 이러한 순차적 대기 시간을 우회하기 위해, 우리는 모든 하위 에이전트 (subagents)가 단일 응답 턴 내에서 동시에 트리거되도록 코디네이터 프롬프트 (coordinator prompt)를 재작성했습니다:

"코드를 순차적으로 작성하지 마십시오. 당신의 첫 번째 응답에서 모든 invoke_subagent 도구 호출을 단일 JSON 배열로 한꺼번에 발행하십시오. 하위 에이전트 1이 완료될 때까지 기다린 후 하위 에이전트 2 또는 3을 호출하지 마십시오."

규칙 3: 계약 주도 개발 (Contract-Driven Development, CDD)
코드를 작성하기 위해 5개의 하위 에이전트를 병렬로 실행할 경우, 이들이 서로의 실시간 파일 상태에 의존한다면 충돌이 발생할 것입니다. 이를 방지하기 위해, 우리는 모델이 API 사양을 포함하는 엄격한 계약 파일 (design.md)을 먼저 작성하도록 강제했습니다. 계약이 수립되어 프리픽스 캐시 (prefix cache)에 로드되었기 때문에, 5개의 하위 에이전트 모두가 이를 병렬로 읽을 수 있었습니다. 이들은 아직 절반만 작성된 다른 파일들을 읽을 필요가 없었으며, 결과적으로 각자의 프롬프트를 짧고 정적이며 캐시 친화적 (cache-friendly)인 상태로 유지할 수 있었습니다.

  1. 성능 결과 및 타임라인
    우리는 세 가지 실행 케이스를 벤치마킹했습니다:

실행 1: 터미널 게임의 순차적 개발 (3개 하위 에이전트: market.py, hacking.py, ship.py).
실행 2: 동일한 터미널 게임의 병렬 개발 (3개 하위 에이전트, 동일 모델).
실행 3: 더 큰 규모의 게임 던전 생성기의 병렬 CDD 개발 (5개 하위 에이전트: dungeon.py, entities.py, combat.py, inventory.py, renderer.py).

동시성 활성화 타임라인 (실행 3 - 5개 병렬 에이전트)
시작 시간 $t=0 ext{s}$ (01:44:13).

월 클락 지속 시간(Wall-clock duration): 87초*.*
시간 [dung] [ent] [comb] [inv] [rend] 활성 요청 수 (Active Reqs) -------------------------------------------------------------------------- t=00s 시작▬▬ [1 Active] t=17s ███████ 시작▬▬ [2 Active] 👥 t=30s ███████ ███████ 시작▬▬ [3 Active] 👥👥 t=41s ███████ 종료▀ ███████ [2 Active] 👥 t=49s ███████ ███████ 시작▬▬ [3 Active] 👥👥 t=56s ███████ 종료▀ ███████ [2 Active] 👥 t=64s ███████ ███████ 시작▬▬ [3 Active] 👥👥 t=76s ███████ 종료▀ ███████ [2 Active] 👥 t=83s ███████ 종료▀ [1 Active] t=87s 종료▀ [0 Active] --------------------------------------------------------------------------
참고: 동일한 vLLM 배치 내에서 최대 4개의 활성 요청이 GPU에서 동시에 처리되는 지속적인 최고 동시성을 관찰했습니다.

GPU Prefix Cache 매치 변화 (Run 3)
아래 라인 플롯은 Prefix Cache Hit Rate (*)가 연속적인 에이전트 턴(agent turns)에 걸쳐 체계적으로 상승하는 반면, GPU KV Cache 메모리 (#)는 평평하고 안전하게 유지되는 방식을 보여줍니다:

[이미지 Placeholder]

  1. 실제 생산성 지표 (대기 시간 대 코드 볼륨)
    여기가 바로 합성 벤치마크가 무너지고 실제 생산성이 드러나는 곳입니다. vLLM 로그에서는 1000 토큰/초 이상의 프리필(prefill) 속도와 113 토큰/초의 생성(generation) 속도를 볼 수 있습니다. 하지만 개발자가 실제로 대기 시간 초당 얼마나 많은 유효 소프트웨어 토큰을 받았을까요?
벤치마크 실행서브 에이전트 수총 코드 생성량월 클락 대기 시간실제 처리량 (대기 시간 t/s)순 효율성 증가율
Run 1: 순차적(Sequential)33,102 토큰97초32.0 t/s1.00x (기준)
Run 2: 병렬(Parallel)33,102 토큰68초45.6 t/s1.43x 🚀
Run 3: 병렬 CDD56,751 토큰87초77.6 t/s2.43x 🚀

핵심 발견 사항:

병렬 대 순차적 (Run 2 vs.

1): 코드베이스의 복잡성을 동일하게 유지하면서, 단순히 병렬 도구 호출 (parallel tool calls)을 수행하도록 프롬프트를 구성하는 것만으로도 개발 시간을 29초 단축했으며, 실제 처리량 (throughput)을 32.0 t/s에서 45.6 t/s로 증가시켰습니다 (1.4x).

CDD 확장 (Run 3 vs. 1): 5개의 서브 에이전트 (subagents)와 CDD를 사용했을 때, 시스템은 2.17배 더 많은 코드 (6,751 토큰 vs 3,102 토큰)를 생성했으며, 작업을 87초 만에 완료했습니다 (이는 순차적 3-에이전트 기준점보다 10초 더 빠른 수치입니다). 실제 대기 시간 처리량 (wait-time throughput)은 77.6 tokens/sec로 급증했습니다 (2.4x).

테스트 실행 오버헤드 (Test Execution Overhead): 단위 테스트 (unit-tests) 및 검증 스크립트 (pytest 및 구문 분석)는 모든 경우에서 로컬에서 0.2초 미만으로 실행되었습니다. 이는 개발자 대기 병목 현상이 100% LLM 생성/동기화 지연 시간 (latency)에 의해 결정된다는 것을 증명하며, 병렬 토큰 처리량 (parallel token throughput)이 최적화해야 할 가장 중요한 변수임을 보여줍니다.

  1. 한계 확장하기 (Scaling the Limits)
    우리의 비대칭 클라이언트 구성 (asymmetric client configuration) 덕분에 메모리 사용량을 매우 낮게 유지할 수 있었기 때문에:
  • 4개의 서브 에이전트가 동시에 생성하는 상황에서도 피크 KV 캐시 할당량 (Peak KV cache allocation)은 7.7% (약 53,200 토큰)에 불과했습니다.
  • docker-compose 설정에서 vLLM의 --max-num-seqs 구성을 5개에서 10개 요청으로 쉽게 늘릴 수 있었습니다.
  • 이는 더 큰 코드베이스 리뷰를 처리할 수 있도록 GPU에 90% 이상의 여유 캐시 메모리 (free cache memory)를 확보해 줍니다.

만약 로컬 소비자용 하드웨어 (RTX 3090/4090 등)에서 멀티 에이전트 작업을 실행하고 있다면, 서브 에이전트를 순차적으로 실행하거나 그들에게 동일한 거대 컨텍스트 윈도우 (context windows)를 제공하는 것을 중단하십시오. 클라이언트 제한을 동적으로 분할하고 계약 파일 (contract file)을 통해 병렬 도구 컴파일을 강제하는 것이 실제 속도에서 엄청난 차이를 만들어냅니다.

여러분은 로컬 멀티 에이전트 컨텍스트 제한을 어떻게 구성하시는지 정말 궁금합니다!

부록: Docker Compose vLLM 설정 예시

다음은 TP=2, FlashInfer, FP8 KV cache, Prefix Caching, 그리고 목표 동시성 제한(concurrency limits)을 적용하여 Dual RTX 3090 vLLM 컨테이너를 실행하는 데 사용한 익명화된 docker-compose.yml 설정입니다:

version: '3.8'
services:
vllm-server:
image: vllm/vllm-openai:latest
container_name: vllm-dual-3090
restart: unless-stopped
ports:
- "8320:8320"
volumes:
# 호스트 스토리지에 캐시 디렉토리 마운트
- ./models-cache:/root/.cache/huggingface
- ./vllm-cache:/root/.cache/vllm
# 중요: Qwen 3.6 27B의 네이티브 템플릿은 OpenAI 스타일의 도구 호출 (tool calls)에 사용할 수 없습니다.
# 올바른 파싱과 사고 블록 (thinking blocks)을 위해 커뮤니티의 froggeric-chat-template을 마운트합니다.
- ./froggeric-chat-template.jinja:/etc/qwen-custom-chat-template.jinja:ro
environment:
- NVIDIA_VISIBLE_DEVICES=all
- VLLM_WORKER_MULTIPROC_METHOD=spawn
- PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True,max_split_size_mb:512
- VLLM_ATTENTION_BACKEND=FLASHINFER
- VLLM_USE_FLASHINFER_SAMPLER=1
- VLLM_API_KEY=${VLLM_API_KEY:-your-secret-api-key}
# 메인보드의 PCIe 슬롯 불일치로 인한 프리징(hangs)을 방지하기 위해 필수
- NCCL_P2P_DISABLE=1
shm_size: "16gb"
ipc: host
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: all
capabilities: [gpu]
command:
- llmfan46/Qwen3.6-27B-uncensored-heretic-v2-Native-MTP-Preserved-GPTQ-Int4
- --served-model-name
- qwen3.6-27b
# 올바른 도구 파싱을 위한 커스텀 채팅 템플릿 활성화
- --chat-template
- /etc/qwen-custom-chat-template.jinja
- --default-chat-template-kwargs
- '{"enable_thinking": true}'
- --dtype
- float16
- --quantization
- gptq_marlin
- --kv-cache-dtype
- "fp8" # KV 캐시 사용량을 낮게 유지하기 위해 중요 (FP8)
- --tensor-parallel-size
- "2" # 두 개의 RTX 3090 GPU 전체에 걸쳐 텐서 병렬성(tensor parallelism) 확장 (TP=2)
- --max-model-len
- "262144" # 네이티브 256K 컨텍스트 제한
- --max-num-seqs
- "10" # 두 배로 늘어난 m

AI 자동 생성 콘텐츠

본 콘텐츠는 r/LocalLLaMA의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.

원문 바로가기
0

댓글

0