서버가 여러 대가 되면 요청을 어느 서버로 보낼지, 서버를 몇 대 둘지를 정해야 한다. 일반적인 웹 서비스에서 쓰던 방법을 그대로 가져오면 LLM 서빙에서는 잘 맞지 않는다. 요청마다 처리 시간이 수십 배씩 차이 나고, 서버의 상태가 CPU 사용률이 아니라 KV 캐시와 대기열에 드러나며, 서버 하나를 새로 띄우는 데 수십 초 이상이 걸리기 때문이다.
라운드로빈이 맞지 않는 이유
라운드로빈은 요청을 서버에 차례로 하나씩 보낸다. 요청마다 처리 비용이 비슷하면 부하가 고르게 나뉜다. LLM 요청은 그렇지 않다. 어떤 요청은 프롬프트 50토큰에 출력 20토큰이고, 어떤 요청은 프롬프트 8,000토큰에 출력 2,000토큰이다. 요청 수를 똑같이 나눠도 한 서버에는 긴 요청이 몰려 KV 캐시가 차고 대기열이 생기는데, 옆 서버는 한가할 수 있다.
요청 수 대신 서버의 현재 상태를 보고 보내야 한다. 볼 수 있는 상태는 여러 가지다.
| 신호 | 의미 | vLLM 지표 예 |
|---|---|---|
| 진행 중인 요청 수 | 지금 생성 중인 요청 | vllm:num_requests_running |
| 대기 중인 요청 수 | 배치에 들어가지 못하고 기다리는 요청 | vllm:num_requests_waiting |
| KV 캐시 사용률 | 새 요청을 받을 메모리 여유 | vllm:kv_cache_usage_perc |
지표 이름은 vLLM의 운영 지표 문서 기준이다. 가장 단순하면서 효과가 큰 방법은 대기 중인 요청이 가장 적은 서버, 같으면 KV 캐시 사용률이 낮은 서버로 보내는 것이다. 대기 중인 요청이 있다는 것은 그 서버가 이미 받을 수 있는 만큼 받았다는 뜻이다.
프리픽스 캐시를 보는 라우팅
프리픽스 캐시를 쓰면 같은 앞부분을 가진 요청을 같은 서버로 보낼수록 프리필을 덜 한다. 대화의 다음 턴, 같은 문서를 두고 나온 여러 질문, 에이전트의 다음 단계가 이전 요청을 처리한 서버로 가야 캐시를 쓴다.
캐시를 보는 라우팅은 대략 세 단계로 정교해진다.
- 세션 고정: 대화 ID나 사용자 ID의 해시로 서버를 정한다. 구현이 쉽고 대화형 서비스에서는 효과가 크다
- 앞부분 해시: 프롬프트 앞부분 몇 블록의 해시로 서버를 정한다. 사용자가 달라도 같은 시스템 프롬프트나 같은 문서를 쓰는 요청이 한 서버에 모인다
- 캐시 상태 추적: 라우터가 서버마다 어떤 앞부분을 캐시하고 있는지 추적하고, 새 요청과 가장 길게 일치하는 서버를 고른다
캐시만 보고 보내면 인기 있는 앞부분을 가진 서버에 부하가 몰린다. 그래서 캐시 일치 길이와 대기열 길이를 함께 점수로 만들어 일치가 길어도 대기열이 길면 다른 서버로 보낸다. 서버 수가 바뀔 때 해시 기반 배정이 통째로 뒤섞이지 않도록 일관 해싱(consistent hashing)을 쓰는 것도 같은 이유다.
확장 신호
오토스케일은 어떤 지표가 기준을 넘으면 서버를 늘리고 내려가면 줄인다. 웹 서버에서 흔히 쓰는 CPU 사용률로는 GPU 추론 서버의 부하를 알 수 없다. nvidia-smi의 GPU 사용률도 커널이 돌고 있던 시간의 비율이라, 요청이 이어지는 동안에는 부하와 상관없이 100% 근처에 머문다. 그래서 둘 다 확장 신호로 쓰지 않는다.
쓸 만한 신호는 사용자가 겪는 지연과, 그 지연에 앞서 움직이는 서버 상태다.
| 신호 | 장점 | 주의할 점 |
|---|---|---|
| 대기 중인 요청 수 | 용량이 모자라기 시작하는 순간 바로 오른다 | 순간적인 버스트에도 오르므로 일정 시간 평균을 본다 |
| KV 캐시 사용률 | 대기열이 생기기 전에 오른다 | 긴 요청 몇 개로도 오를 수 있다 |
| TTFT p99 | 사용자가 겪는 지연 그 자체다 | 이미 나빠진 뒤에 오른다. 확장에 시간이 걸리면 늦다 |
| 요청률 | 리틀의 법칙으로 필요한 서버 수를 바로 계산할 수 있다 | 요청 길이 분포가 바뀌면 같은 요청률에도 필요한 용량이 달라진다 |
실무에서는 대기 중인 요청 수나 KV 캐시 사용률로 서버를 늘리고, TTFT p99로 그 기준값이 맞는지 검증한다. 기준값은 부하 테스트에서 지연 목표를 넘기 직전의 상태로 정한다. 예를 들어 테스트에서 서버 한 대가 TTFT 목표를 지키는 동안 대기 중인 요청이 평균 두 개를 넘지 않았다면, 그 값을 확장 기준으로 삼는다.
콜드 스타트
LLM 추론 서버는 새로 띄우는 데 오래 걸린다. 과정마다 시간이 든다.
- 노드 확보: 클라우드에서 GPU 노드를 새로 받으면 수 분이 걸릴 수 있다
- 컨테이너 이미지: 추론 엔진 이미지는 수 GB라 처음 받는 노드에서는 내려받는 시간이 든다
- 가중치 로드: 8B 모델 BF16 가중치 16GB를 초당 2GB로 읽는 저장소에서 가져오면 8초, 70B 모델 141GB면 70초 이상이다
- 엔진 초기화: KV 캐시 메모리를 잡고, 커널을 준비하고, CUDA 그래프를 캡처하는 데 시간이 든다
이 시간 동안 늘어난 부하는 기존 서버가 버텨야 한다. 줄이는 방법은 각 단계를 미리 해 두는 것이다. 이미지를 노드에 미리 받아 두고, 가중치를 노드의 로컬 NVMe에 캐시하고, 최소 서버 수를 피크 직전 수준으로 유지하고, 하루 중 트래픽이 오르는 시간이 일정하다면 일정에 맞춰 미리 늘린다. 확장 기준도 콜드 스타트 시간을 고려해 낮게 잡는다. 서버가 뜨는 데 2분이 걸린다면 지금 부하가 2분 동안 더 늘어도 기존 서버가 목표를 지킬 수 있는 지점에서 확장을 시작해야 한다.
축소할 때 요청 비우기
서버를 줄일 때 진행 중인 요청을 끊으면 사용자는 생성 도중에 응답을 잃는다. 출력이 수천 토큰인 요청은 끝나는 데 수십 초가 걸리므로 축소에는 비우기 절차가 필요하다.
- 라우터가 그 서버로 새 요청을 보내지 않는다
- 진행 중인 요청이 끝날 때까지 기다린다. 최대 생성 길이와 토큰 간격으로 최대 대기 시간을 계산해 둔다
- 요청이 모두 끝나거나 대기 시간이 다 되면 서버를 내린다
쿠버네티스라면 종료 유예 시간을 이 최대 대기 시간보다 길게 둬야 한다. 기본 유예 시간은 긴 생성을 끝내기에 짧다.
축소는 확장보다 천천히 한다. 부하가 잠깐 줄었다고 바로 줄이면 다시 늘 때 콜드 스타트를 또 치른다. 축소 기준을 확장 기준보다 낮게 두고 그 상태가 일정 시간 이어질 때만 줄인다.