배칭과 다중 사용자
한 번에 하나의 요청은 모든 데스크톱 세션이 시작하는 방식입니다 — 그리고 GPU를 낭비합니다. 서빙의 게임 전부는 배칭입니다. 가중치를 통과하는 패스 하나가 여러 요청을 한꺼번에 섬기게 만드는 것. 이 페이지는 왜 그것이 작동하는지, 연속 배칭과 페이징드 어텐션이 실제로 무엇인지, 언제 진짜 서빙 엔진이 필요한지, 그리고 어떻게 사이징하는지를 다룹니다.
직렬 서빙이 GPU를 낭비하는 이유
속도 공식을 떠올려 보세요. 디코드된 토큰마다 활성 가중치 전체를 사실상 한 번 읽습니다(속도 참고). 스트림 하나를 서빙하면 GPU는 사용자 한 명의 토큰 하나를 위해 그 읽기 전체를 합니다. 이제 스트림 여덟을 같은 순전파에 배칭하세요. 가중치는 한 번 읽히고 여덟 시퀀스에 쓰입니다 — 디코드가 연산 병목이 아니라 대역폭 병목이므로 추가 산술은 거의 공짜입니다. 스트림별 속도는 거의 안 떨어지는 반면 총 처리량은 거의 선형으로 오릅니다. 대역폭이나 KV 메모리가 포화될 때까지. 이 단일 사실이 로컬 서빙의 경제학 전부입니다.
정적 배칭 대 연속 배칭
나이브 버전 — 정적 배칭 — 은 함께 도착한 요청들을 묶어 모든 구성원이 끝날 때까지 배치를 돌립니다. 작동합니다. 형편없이:
| 정적 배칭 | 연속 배칭 | |
|---|---|---|
| 배치 구성 | 그룹이 모였을 때 한 번 | 모든 디코드 단계마다 재평가 |
| 진행 중 합류 | 불가 — 다음 배치 대기 | 가능, 요청이 도착하면 슬롯이 참 |
| 긴 배치 속의 짧은 요청 | 가장 긴 것이 끝날 때까지 봉쇄 | 끝나고 즉시 슬롯 반환 |
| 꼬리 레이턴시 | 나쁨 (head-of-line 봉쇄) | 좋음 |
연속(반복 수준) 배칭 — vLLM과 SGLang의 기본 — 이 나이브 서버가 무너지는 자리에서 그 엔진들이 실제 트래픽을 버티는 이유입니다.
페이징드 어텐션: KV 파편화 해결
활성 요청마다 KV 캐시 메모리가 필요합니다(KV 캐시 참고). 옛 방식은 각 요청의 최대 컨텍스트를 미리 예약했습니다 — 그래서 2k 토큰을 쓰는 챗이 잠기고 아무도 못 쓰는 32k 블록을 쥘 수 있었습니다. KV 풀의 대부분이 놀았습니다. 페이징드 어텐션은 KV를 OS가 메모리를 페이징하듯 수요에 따라 할당되는 고정 크기 블록에 저장합니다. 낭비가 대다수(vLLM 저자들이 논문에서 60–80% 보고)에서 몇 퍼센트로 떨어지고, 동시 시퀀스가 몇 배 더 들어가고, 처리량이 따라갑니다. 같은 아이디어가 이제 서빙 엔진 전반의 표준입니다.
당신에게 드는 비용: 사용자에게 보이는 것은 없습니다 — 할당자 변경입니다. 같은 GPU에서 더 높은 동시 용량이 보일 뿐.
진짜 서빙 엔진이 필요할 때
| 워크로드 | 엔진 |
|---|---|
| 당신과 가족, 순차적 챗 몇 개 | Ollama / LM Studio — 이미 충분 |
| 개발자 몇 명이 동시에 만지작 | 병렬 슬롯의 llama-server, 또는 Ollama 병렬 환경변수 |
| 제품 트래픽, 팀 챗, 실제 사용자 | vLLM 또는 SGLang |
| 긴 시스템 프롬프트를 공유하는 에이전트 함대 | SGLang (프리픽스 캐싱) 또는 vLLM |
이 포럼의 게임 코너가 마지막 행의 정전 사례입니다. 체스와 장기 대국은 몇 초마다 수를 요청하는 에이전트 여럿을 돌립니다 — 작은 프롬프트, 많은 스트림, 공유 시스템 프롬프트. 그것이 배칭이 있는 서빙 엔진을 원하는 정확히 그 워크로드고, 루프 도는 ollama run이 아닙니다(에이전트 참고).
처리량 대 레이턴시
모든 노브는 하나를 다른 것과 맞바꿉니다. 배치가 커지면 총 tok/s는 오르고 스트림별 속도는 살짝 떨어지며, 대기열은 부하 상황의 TTFT를 올립니다. 1인용 코딩 어시스턴트는 사람의 스트림이 절대 줄서지 않도록 동시성을 1–2로 묶고 싶어 합니다. 30명의 챗 서비스는 배치를 꽉 채우고 싶어 합니다. 튜닝 전에 어느 쪽인지 정하세요(TTFT 대 디코드의 별도 측정으로는 서빙 참고).
공짜 점심에 가장 가까운 것은 프리픽스 캐싱(SGLang)입니다. 반복되는 긴 프롬프트가 0에 가까운 프리필로 바뀌는데, 그것이 대부분의 에이전트 워크로드입니다.
실용 노브
# llama.cpp: 병렬 디코드 슬롯 (컨텍스트는 슬롯에 걸쳐 분할)
llama-server -m model.gguf -c 32768 -np 4
# Ollama: 모델당 병렬 요청 (설정 후 서비스 재시작)
export OLLAMA_NUM_PARALLEL=4
# vLLM: 중요한 두 가지
vllm serve model --max-num-seqs 32 --gpu-memory-utilization 0.90
대기열은 이 모든 것의 상류에서 일어납니다. 모든 슬롯이 바쁘면 새 요청은 기다립니다. 감이 아니라 대기 깊이와 TTFT를 지켜보세요.
상류에 한계를 설정하세요. 연결 상한이 있는 리버스 프록시는 빠르고 정직하게 실패하는 반면, 깊은 대기열은 느리고 신비하게 실패합니다.
용량 계획, 대략적으로
가중치 너머에서 요청이 비용을 치르는 것은 컨텍스트 전체에 걸친 토큰마다의 KV 캐시입니다:
KV bytes/token ≈ 2 × layers × kv_heads × head_dim × bytes_per_value
계산 예제, 전형적 8B 덴스 모델 (32층, KV 헤드 8개, 헤드 차원 128, fp16 값):
2 × 32 × 8 × 128 × 2 B ≈ 토큰당 131 KB → 2k 토큰 대화 ≈ KV 256 MiB → 그런 챗 10개 ≈ 가중치 ~5 GB 위에 2.5 GB.
(2는 K와 V입니다. KV 캐시 참조.) 그래서 롱 컨텍스트 에이전트는 챗보다 요청당 훨씬 비쌉니다. 대략적 편안함 표, 헤지 아주 세게 — 챗 길이 컨텍스트, 자릿수만:
| GPU | 모델 | 대략적 동시 가벼운 챗 |
|---|---|---|
| 8 GB | 7–8B Q4 | 1–3 |
| 24 GB | 8B Q4 | ~10–30 |
| 24 GB | 32B Q4 | ~3–8 |
| 48 GB | 70B Q4 | ~5–15 |
실제 프롬프트로 측정하세요. 컨텍스트 길이가 이 숫자들을 다른 어떤 변수보다 크게 움직입니다.
포화 신호
- 하루가 지나며 TTFT가 밀리초에서 초로 오르는 것.
- 대기 깊이가 0 위에 지속적으로 있는 것. 첫 토큰 전에 요청이 기다림.
- 배치가 꽉 차며 스트림별 tok/s가 처지는 것.
- vLLM 로그에 선점(preemption)이나 KV 재계산 — 모델이 느린 게 아니라 메모리 압력.
이중 하나라도: 컨텍스트를 묶고, KV 캐시를 양자화하고, GPU를 추가하세요 — 저렴한 순서대로. 사용자가 크게 불평하기 전에.
하루에 걸친 TTFT와 대기 깊이를 그려 보세요 — 꾸준한 상승은 용량을, 톱니는 대기열로 흡수할 수 있는 폭발을 뜻합니다.
렌트 대 구매, 스케치
집필 시점 기준으로, 렌트 24GB급 GPU는 대략 시간당 $0.50–2이고, 중고 24GB 카드는 몇백 달러입니다 — 실제 생성 기준 대략 300–1,000 시간쯤에서 손익분기. 로컬은 프라이버시, 레이턴시, 레이트 리밋 없음(왜 로컬인가의 프레임)에서 이기고, 렌트는 폭발적 수요와 소비자 카드가 담지 못하는 80GB급 모델에서 이깁니다(무엇이 어디 들어가는지는 VRAM 계산 참조). 대부분의 사람은 자기 시간을 과대평가합니다 — 결정 전에 실제 일주일을 계측하세요.