6 분 소요

🖥️ Claude Code·Codex 를 온프렘으로 옮기기 5부작 — API 요금이 감당 안 될 때, 코딩 에이전트를 사내 GPU 위로 통째로 내리는 이야기예요. 먼저 무엇을 대체해야 하는지(모델·서빙·하네스 3층)와 비용 손익분기를 잡고(1편), 코딩 에이전트급 오픈웨이트 모델을 고른 뒤(2편), GPU·VRAM·양자화·vLLM 으로 실제로 띄우고(3편), 로컬 모델을 에이전트 CLI 에 물려 툴을 쓰게 만든 다음(4편), "클라우드와 완벽히 동일하게" 쓰려면 무엇을 검증해야 하고 어떤 갭이 남는지(5편)까지 이어집니다. 전체 5편.
  1. 왜, 그리고 무엇을 갖춰야 하나 — 3층 구조와 손익분기
  2. 모델 고르기 — 어떤 오픈웨이트가 코딩 에이전트급인가
  3. 하드웨어와 서빙 — GPU·VRAM·양자화·vLLM 로 띄우기지금 글
  4. 하네스 연결 — 로컬 모델을 에이전트 CLI 에 물리기
  5. "완벽히 동일"의 조건 — 검증·남는 갭·하이브리드 운영

Summary

2편에서 후보 모델을 좁혔어요. 이번엔 그 모델을 실제 GPU 위에 올리는 이야기예요. 여기서 막연했던 “GPU 사야지”가 구체적인 VRAM 숫자와 장비 구성으로 바뀝니다.

핵심은 딱 두 개예요. ① 이 모델이 몇 GB 를 먹나(그래서 GPU 몇 장이 필요한가), 그리고 ② 그걸 어떻게 API 로 띄우나. 이 둘만 손에 잡히면 나머지는 옵션 조정이에요.

💡 이 글에서 다루는 것

  • VRAM 어림셈 — 파라미터 × 바이트수 + 여유분
  • 양자화(FP8·4bit)로 메모리 반·반의반 줄이기
  • 긴 컨텍스트가 왜 메모리를 먹나 — KV 캐시
  • 텐서 병렬 — 한 모델을 여러 GPU 에 쪼개 올리기
  • vLLM 으로 OpenAI 호환 엔드포인트 띄우기
  • 처리량·동시성, 그리고 손익분기 재계산



1. VRAM 어림셈 — 이 모델, 몇 GB 먹나

가장 먼저 필요한 감각은 “이 모델을 올리려면 GPU 메모리가 몇 GB 냐”예요. 놀랍도록 단순한 곱셈으로 시작해요.

가중치 메모리 ≈ 파라미터 수 × 파라미터당 바이트 수

파라미터당 바이트 수는 정밀도(=양자화 수준)로 정해져요. FP16 이면 2바이트, FP8 이면 1바이트, 4bit 이면 0.5바이트예요. 여기에 KV 캐시·활성값 같은 실사용 여유를 20% 쯤 얹어서 어림합니다.

작은 함수로 감을 잡아볼게요.

def vram_gb(params_b: float, bytes_per_param: float, overhead: float = 1.2) -> float:
    # params_b: 총 파라미터(단위 10억). MoE 는 '활성'이 아니라 '총량'을 넣어야 함
    # bytes_per_param: FP16=2, FP8=1, 4bit=0.5
    # overhead: KV 캐시·활성값 등 실사용 여유 (대략 1.2배)
    return params_b * bytes_per_param * overhead

# 예시 — 총 480B MoE 를 정밀도별로
for name, bpp in [("FP16", 2), ("FP8", 1), ("4bit", 0.5)]:
    print(f"{name:>5}: 약 {vram_gb(480, bpp):>5.0f} GB")
 FP16: 약  1152 GB
  FP8: 약   576 GB
 4bit: 약   288 GB

이 숫자를 실제 GPU 로 옮겨볼게요. 데이터센터 GPU 한 장이 80GB 안팎(예: 80GB급)이라, 8장을 묶은 한 노드가 대략 640GB 예요.

  • FP16(1152GB) — 한 노드로 부족. 두 노드를 이어야 해요.
  • FP8(576GB) — 한 노드에 빠듯하게 들어가요.
  • 4bit(288GB) — 한 노드에 여유 있게. 컨텍스트용 KV 캐시 공간도 넉넉해져요.

💡 여기서 2편의 MoE 이야기가 왜 중요했는지 드러나요. VRAM 은 활성 파라미터가 아니라 총 파라미터로 계산해야 해요. “480B-A35B” 는 속도는 35B급이어도, 메모리는 480B 를 다 담아야 합니다. 이 착각이 견적을 반토막으로 왜곡하는 단골 실수예요.



2. 양자화 — 메모리를 반, 다시 반의반으로

위 표에서 봤듯이 정밀도를 낮추면 메모리가 그대로 줄어요. 이게 양자화예요. 가중치를 더 적은 비트로 표현하는 거죠.

정밀도 메모리 품질 체감
FP16 기준(×1) 원본
FP8 절반(×0.5) 거의 무손실
4bit 1/4(×0.25) 살짝 저하
  • FP8 — 요즘 서빙의 실질 표준에 가까워요. 메모리는 절반인데 품질 저하가 거의 없어서, “일단 FP8” 로 두면 대개 무난해요.
  • 4bit(AWQ·GPTQ·GGUF 등) — 메모리를 확 줄여 더 작은 장비에 큰 모델을 우겨넣을 때 유용해요. 다만 코딩처럼 정밀함이 중요한 작업에선 미세한 품질 저하가 티가 날 수 있어서, 파일럿으로 성공률을 꼭 비교하고 넘어가야 해요.

⚠️ 양자화는 공짜 점심이 아니에요. “4bit 로 큰 모델 올렸더니 VRAM 은 아꼈는데 에이전트 성공률이 떨어지더라” 는 흔한 시나리오예요. 메모리 절약과 품질은 트레이드오프라, 아낀 메모리로 더 큰 모델을 담을지 vs 같은 모델을 고정밀로 담을지는 파일럿 결과로 정하세요.



3. 긴 컨텍스트는 KV 캐시로 메모리를 먹는다

2편에서 “긴 컨텍스트는 하드웨어 비용과 직결된다”고 했죠. 그 정체가 KV 캐시예요.

모델은 이미 읽은 토큰들의 중간 계산 결과(key·value)를 캐시에 쌓아두고 다음 토큰을 생성해요. 그래서 컨텍스트가 길수록, 동시 요청이 많을수록 KV 캐시가 커지고, 이건 가중치와 별도로 VRAM 을 먹어요.

  • 가중치는 한 번 올리면 고정이에요.
  • KV 캐시는 “컨텍스트 길이 × 동시 사용자 수”에 비례해 출렁여요.

그래서 “왜 나 혼자 쓸 땐 되던 게 팀이 붙으니 메모리 부족(OOM)이 나지?” 의 답이 대개 여기예요. 동시 요청이 늘면 KV 캐시가 부풀어 터지는 거죠.

💡 그래서 서빙 설정에서 최대 컨텍스트 길이(--max-model-len)를 필요 이상 크게 잡지 않는 것도 중요한 튜닝이에요. 128K 를 열어두면 그만큼 KV 캐시 상한이 커져서 동시 처리량이 줄어요. “우리 팀이 실제로 쓰는 창”에 맞춰 잡는 게 메모리를 아끼는 길입니다.



4. 텐서 병렬 — 한 모델을 여러 GPU 에 쪼개기

FP8 480B 가 576GB 라면 80GB GPU 한 장엔 절대 안 들어가요. 그래서 한 모델을 여러 GPU 에 나눠 올려요. 가장 기본이 텐서 병렬(tensor parallelism) 이에요.

  • 각 레이어의 가중치를 여러 GPU 에 가로로 쪼개 올리고, 토큰 하나를 처리할 때 GPU 들이 동시에 계산하고 결과를 합쳐요.
  • 그래서 8장을 묶으면 메모리가 8배가 될 뿐 아니라 계산도 나눠져요. 대신 GPU 간 통신이 필요해서 고속 인터커넥트(NVLink 등) 가 성능을 크게 좌우해요.

vLLM 에선 이걸 옵션 하나로 지정해요. --tensor-parallel-size 8 이면 “이 모델을 8장에 쪼개 올려라” 예요. 노드를 넘어가면 파이프라인 병렬까지 얹지만, 팀 한두 개 규모라면 대개 한 노드 안 텐서 병렬로 충분합니다.



5. vLLM 으로 띄우기 — OpenAI 호환 엔드포인트

이제 실제로 띄워볼게요. vLLM 은 오픈소스 추론 엔진이고, 뜨는 순간 OpenAI 호환 API 를 열어줘요. 이게 4편에서 하네스를 붙일 접점이에요.

모델을 서빙하는 기본 명령이에요.

vllm serve <model-repo>/<coder-moe> \
  --tensor-parallel-size 8 \
  --quantization fp8 \
  --max-model-len 131072 \
  --gpu-memory-utilization 0.92 \
  --served-model-name local-coder \
  --host 0.0.0.0 --port 8000
  • --tensor-parallel-size 8 — 8장에 쪼개 올리기.
  • --quantization fp8 — FP8 로 메모리 절반.
  • --max-model-len — 최대 컨텍스트. KV 캐시 상한과 직결(3장 참고).
  • --served-model-name — 하네스에서 부를 모델 이름. local-coder 로 별칭을 줬어요.

뜨고 나면 OpenAI 형식으로 그대로 호출돼요. 잘 떴는지 curl 로 확인합니다.

curl http://your-gpu-host:8000/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "local-coder",
    "messages": [{"role": "user", "content": "파이썬으로 피보나치 함수 하나만 짜줘"}]
  }'

응답 JSON 이 OpenAI 챗 컴플리션과 같은 스키마로 떨어지면 성공이에요. 이 엔드포인트(http://your-gpu-host:8000/v1)가 앞으로 우리 코딩 에이전트의 두뇌 주소가 됩니다.

✅ 여기까지 오면 3층 중 서빙이 초록불이에요. “모델 가중치 → GPU → OpenAI 호환 API” 파이프가 실제로 연결됐어요. vLLM 대신 SGLang 같은 엔진도 같은 자리를 채워요 — 처리량·기능이 조금씩 달라서 파일럿 때 둘 다 재보면 좋아요.



6. 처리량·동시성 — 팀이 붙어도 견디게

나 혼자 쓸 땐 반응 속도만 보면 되지만, 팀이 동시에 붙으면 두 지표가 경험을 좌우해요.

지표 좌우하는 것
첫 토큰 지연 요청 후 첫 글자까지 프롬프트 길이·부하
처리량 초당 총 생성 토큰 배치·GPU 수·KV 여유

vLLM 은 여러 요청을 동적으로 묶어(continuous batching) GPU 를 촘촘히 채우는 데 강해요. 덕분에 사용자가 늘어도 GPU 가 놀지 않고 처리량이 잘 올라가요. 다만 3장에서 봤듯 동시성이 오르면 KV 캐시가 부풀어 한계에 부딪히니, “동시 N명 × 평균 컨텍스트” 를 미리 잡아 최대 컨텍스트와 GPU 여유율을 튜닝해두는 게 운영 포인트예요.



7. 손익분기 재계산 — 이제 숫자를 넣는다

1편에서 “월 API 지출 > 장비 월 고정비” 면 온프렘이 이긴다고 했죠. 이제 그 고정비에 넣을 항목이 구체화됐어요.

장비 월 고정비 ≈ (장비 구입비 ÷ 감가상각 개월)   # 예: 36개월
              + 전력·냉방 (kW × 24h × 30일 × 전기요금)
              + 운영 인건비 (모델·GPU 돌보는 시간)
  • 장비 구입비 — GPU 노드는 목돈이에요. 보통 36개월 감가상각으로 월할 잡아요.
  • 전력·냉방 — GPU 노드는 상시 수 kW 를 먹어요. 24시간 돌리면 전기·냉방이 무시 못 할 항목이 돼요.
  • 운영 인건비1편에서 강조한 항목. 사면 끝이 아니라 계속 돌봐야 해서, 이걸 빼면 계산이 장밋빛으로 왜곡돼요.

💡 실전 판단은 이렇게 해요. 최근 3~6개월 실제 API 청구서의 월 평균을 뽑고, 위 월 고정비와 비교하세요. API 평균이 고정비를 꾸준히 넘고 있고 사용량이 줄 기미가 없다면 온프렘이 이깁니다. 애매하면, 다 사기 전에 GPU 를 몇 달 빌려(rent) 파일럿으로 실제 손익을 재보는 걸 추천드려요.

이제 두뇌(모델)를 GPU 에 올리고 API 주소까지 얻었어요. 남은 건 이 API 에 손발(도구) 을 달아 진짜 에이전트로 만드는 일이에요.

일단 오늘은 여기까지…..
다음 글에서는 이 OpenAI 호환 엔드포인트를 오픈소스 에이전트 CLI 에 물려, 로컬 모델이 실제로 파일을 읽고 고치게 만들어볼게요.


← 이전 글: (2/5) 모델 고르기 — 어떤 오픈웨이트가 코딩 에이전트급인가다음 글 →: (4/5) 하네스 연결 — 로컬 모델을 에이전트 CLI 에 물리기