자체 호스팅 Claude 코드가 기대보다 15% 느린 이유
출처: Dev.to

업데이트 (2026-05-14). SimpleEngine prefix‑cache 패치가
Finding #2 로 upstream에 반영되었습니다:
vllm-mlx PR #523 (병합됨).
최근 vllm-mlx 빌드를 사용하고 있다면, 이미 수정이 적용돼 있습니다 — 별도의 로컬 패치가 필요 없습니다. 아래 walkthrough는 패치가 무엇을 하는지, 왜 필요한지 이해하는 데 여전히 유용합니다.
{: .prompt-info }
업데이트 (2026-05-18) — 실제로 실행할 경우 주의할 두 가지 추가 포인트:
Sparse‑MoE Coder 모델에 대해 json_schema response_format을 엄격히 사용하지 마세요. 같은 vllm-mlx 인스턴스에 대해 구조화된 출력을 사용하는 LangChain(또는 OpenAI 호환 클라이언트)을 실행한다면, LangChain 기본값인 "json_schema" 대신 with_structured_output(schema, method="json_mode")를 사용하세요. 엄격 모드는 문법 제약 디코딩을 트리거하는데, Qwen3‑Coder‑30B‑A3B에서는 호출당 5분 이상 대기하게 만들고, 디코더가 멈추어 모든 대기 중인 요청(Claude Code 세션 포함)을 서버가 재시작될 때까지 차단합니다. 이 문제는 upstream에
vllm-mlx#546 로 보고되었습니다.
PR #523 은 단일 슬롯 시스템‑KV 캐시를 고칩니다. 다중 슬롯 변형도 원하실 겁니다. Claude Code 하위 에이전트(Explore, Plan, 일반 목적)는 서로 다른 툴 세트를 가지고 있어 각각의 시스템 프리픽스가 메인 에이전트와 다릅니다. 단일 슬롯 스냅샷을 사용하면, 모든 하위 에이전트 디스패치가 메인 에이전트 캐시를 내보내고 그 반대도 마찬가지이며, 매 턴마다 ~28K 토큰의 콜드 프리필을 전부 지불하게 됩니다. 다중 슬롯 LRU 후속 작업은 현재 로컬에만 존재하며, upstream PR이 진행 중입니다.
{: .prompt-warning }
TL;DR
Mac Studio에서 self‑hosted vllm-mlx 백엔드와 함께 Claude Code를 실행했습니다. 콜드 턴은 약 108초가 걸렸고, 후속 요청은 거의 동일하게 오래 걸렸습니다. 시스템 프롬프트는 바이트‑안정적이며, 정상적인 LLM 엔진이라면 프리픽스를 캐시해야 합니다.
두 가지 필수 발견을 통해 속도를 크게 끌어올렸습니다:
-
Claude Code는 매 턴마다
x-anthropic-billing-header값을 회전시켜 시스템 블록에 삽입합니다. 사용자에게 보이는 시스템 프롬프트는 변하지 않지만, 엔진이 캐시 조회를 위해 해시하는 바이트는 매 요청마다 달라집니다. 따라서 프리픽스 캐시가 100% 미스합니다. 프록시 레이어에서 이 헤더를 제거하면 캐시가 정상적으로 동작합니다. -
vllm-mlx의
SimpleEngine은 요청 간 KV 상태를 유지하지 않습니다. 회전 헤더를 제거했더라도, SimpleEngine을 패치해 시스템 프리픽스를 턴 사이에 실제로 캐시하도록 해야 합니다 — 히트 시 스냅샷을 복원하고, 접미사만 프리필하는 작은 단일 슬롯, 해시‑키 기반 캐시가 필요합니다.
두 가지를 합치면 108초 턴 → 7‑8초 후속, 13‑15배 속도 향상이 동일 하드웨어·동일 모델에서 실현됩니다.
108s → 7‑8s
워밍 턴 실시간(벽시계) 비교: 패치 전후
13‑15×
후속 속도 향상 (동일 하드웨어·모델)
81 bytes
회전 헤더 텍스트가 턴당 100초 이상을 잡아먹었습니다.
설정
flowchart LR
CC[Claude Code CLI] -->|/v1/messages
system + tools + msgs
+ rotating cch=...| CCR[claude-code-router]
CCR --> Shim["Shim**(1) x-anthropic-billing-header 제거**\n(2) tool‑call 스트림 버퍼링**"]
Shim -->|byte‑stable system prefix| VLLM[vllm-mlx server]
VLLM --> SE["SimpleEngine**(3) system‑prefix KV 캐시**\nHIT: 프리필 건너뛰기\nMISS: 프리필 + 스냅샷"]
SE -->|스트림 토큰| CC
style Shim fill:#1e40af,color:#fff
style SE fill:#7c2d12,color:#fff
세 번호가 붙은 지점이 속도 향상의 핵심입니다. (1)과 (3)을 제거하면 다시 100초 이상의 턴이 됩니다.
- 백엔드: Mac Studio(96 GB)에서
Qwen2.5-Coder-32B-Instruct-8bit을 제공하는 vllm-mlx. - 프론트 도어: Anthropic
/v1/messagesAPI를 노출하고 vllm-mlx로 프록시하는 작은 FastAPI shim. - 라우팅:
claude-code-router가 Claude Code의 아웃바운드 호출을 베어러 토큰과 함께 shim URL로 변환합니다. - 클라이언트: Claude Code CLI.
엔드‑투‑엔드로 아키텍처는 정상 작동했습니다. 툴 호출, 스트리밍, 출력 품질 모두 문제 없었지만 느렸다는 점이 핵심이었습니다.
참고로 Claude Code의 프롬프트는 상당히 큽니다. 이 설정에서 캡처된 요청들을 기준으로 캐시 가능한 프리픽스(시스템 지시문 + 툴 정의 블록)는 약 23 000 토큰(≈5.6 K 시스템 + ≈17.6 K 툴, 23개 툴 세트) 정도 됩니다. 프리픽스 캐시가 정상 작동한다면 매 턴마다 새 사용자 메시지와 대화의 꼬리 부분만 처리하면 되며, 보통 수백 토큰 정도입니다. 캐시가 없으면 엔진은 매 턴마다 ~23 K 토큰을 다시 프리필하게 됩니다. 32 K 컨텍스트 모델이라면 대화와 출력에 약 9 K 토큰의 여유가 남지만, 매 턴마다 프리픽스 작업을 버린다면 그 여유는 의미가 없습니다.
기대와 실제
| 콜드 턴 | 워밍 턴 | |
|---|---|---|
| 기본 vllm-mlx, shim 없음 | 108 s | ~100 s |
| shim이 billing 헤더만 제거 | 105 s | ~70 s |
| shim이 헤더 제거 + SimpleEngine KV‑cache 패치 | 108 s | 7‑8 s |
콜드 턴은 첫 요청이므로 캐시가 존재하지 않아
