(3/5) 하드웨어와 서빙 — GPU·VRAM·양자화·vLLM 로 모델 띄우기
- 왜, 그리고 무엇을 갖춰야 하나 — 3층 구조와 손익분기
- 모델 고르기 — 어떤 오픈웨이트가 코딩 에이전트급인가
- 하드웨어와 서빙 — GPU·VRAM·양자화·vLLM 로 띄우기 ← 지금 글
- 하네스 연결 — 로컬 모델을 에이전트 CLI 에 물리기
- "완벽히 동일"의 조건 — 검증·남는 갭·하이브리드 운영
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바이트. 4분의 1까지 줄였어요.
이렇게 정밀도를 낮춰 메모리를 줄이는 걸 양자화라고 불러요. 원리와 품질 트레이드오프는 바로 다음 2장에서 제대로 풀 테니, 지금은 “정밀도를 낮추면 그만큼 메모리가 준다” 하나만 잡고 가면 돼요.
이 곱셈에 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 를 담으니, 8장을 묶은 한 노드가 대략 640GB 예요. 이 640GB 라는 그릇에 위 숫자가 들어가는지 보면 됩니다.
FP16(1152GB)— 한 노드로는 부족해요. 두 노드를 이어야 하죠.FP8(576GB)— 한 노드에 빠듯하게 들어가요.4bit(288GB)— 한 노드에 여유 있게. 컨텍스트용 KV 캐시 공간도 넉넉해져요.
💡 여기서 2편의 MoE 이야기가 왜 중요했는지 드러나요. VRAM 은 활성 파라미터가 아니라 총 파라미터로 계산해야 해요. “480B-A35B”(총 480B·토큰당 활성 35B)는 속도는 35B급이어도, 메모리는 480B 를 다 담아야 합니다. 이 착각이 견적을 반토막으로 왜곡하는 단골 실수예요.
2. 양자화 — 메모리를 반, 다시 반의반으로
1장에서 예고한 양자화를 이제 제대로 볼게요. 앞서 봤듯 정밀도를 낮추면(=가중치를 더 적은 비트로 표현하면) 메모리가 그만큼 줄어요. 그게 양자화의 전부예요. 남는 질문은 하나, “얼마나 줄이면 품질이 어디서부터 티가 나느냐”죠.
| 정밀도 | 메모리 | 품질 체감 |
|---|---|---|
| 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 에 물리기