vLLM 나 HuggingFace TGI 와 같은 추론 엔진은 다음으로 구성됩니다:
- 요청에서 다음 토큰을 계산하는 실제 작업을 수행하는 워커 (worker)
- 처음 도착했을 때 요청이 추가되는 큐
- 큐에서 요청을 가져와 워커로 이동시키는 스케줄러 (scheduler)
왜 여기에 큐가 필요한가요? GPU 의 계산을 개별 요청이 아닌 배치 (batch) 방식으로 수행할 때 성능과 리소스 효율성이 더 뛰어나기 때문입니다. 이 백엔드 큐는 스케줄러가 여러 요청을 선택하여 같은 배치에 배치하여 처리하도록 허용합니다.
일반적으로 각 추론 엔진은 단일 모델만 서비스하며, 우리는 다른 모델을 병렬로 실행하는 여러 배포를 가지고 있습니다.
단일 사용자 "A" 가 많은 수의 요청을 보낼 때, 큐는 빠르게 채워집니다. "B" 와 "C" 같은 다른 사용자는 자신의 요청을 짧은 시간 후에 보내도 동일한 모델을 사용할 수 없습니다 (사용자 "A" 의 모든 요청이 처리될 때까지). 이 그림은 vLLM 을 추론 엔진으로 초점을 맞추고 있지만, 문제는 더 일반적이며 다른 백엔드에도 적용됩니다.
TNG 에서 사용자들은 vLLM 백엔드로 직접 요청을 보내지 않지만 API 서버 (우리가 "LLM-Server"라고 부름) 로 요청을 보냅니다. 여기서는 각 사용자 (및 모델) 에 대해 별도의 큐를 가질 수 있으며, FIFO(첫 번째 들어오기 - 첫 번째 나가기) 가 아닌 모든 사용자 큐를 순환하는 스케줄러가 있습니다. 이는 일부 공정한 스케줄링을 달성합니다: 예를 들어, 위의 그림에서 사용자 B 와 C 는 사용자 A 의 첫 번째 요청이 이미 스케줄링되었을 때 조금 더 늦게 자신의 요청을 보냅니다. 그 시점에서 사용자 A 에서 세 개의 요청이 이미 일정 시간 동안 대기 중이었지만, 사용자 C 는 사용자 A 의 한 요청이 완료될 때까지 기다려야 합니다.
핵심 아이디어는: 우리 자체 구성 요소에서 다른 사용자의 요청을 우선시하고 추론 백엔드에서는 아닙니다! 일반적으로, 요청이 추론 엔진에 보내진 후 그 순서는 변경할 수 없으므로, LLM-Server 에서 완전한 통제권을 가지고 있을 때 올바른 순서로 가져와야 합니다.
"공정한" 스케줄링이 무엇인지 결정하기 위해 다른 측면을 고려할 수 있습니다. 위의 예에서, 단일 요청을 가진 새로운 사용자는 연속으로 두 번 이상 서비스된 사용자보다 먼저 서비스되어야 합니다. 이는 "요청 수" 수준에서의 "공정성"입니다. 하지만 처리 시간도 고려할 수 있습니다: 요청이 백엔드를 얼마나 오랫동안 점유하는가? 긴 프롬프트와 긴 생성은 다른 사용자에게 더 오랜 시간을 LLM 을 "차단"하므로, 짧은 요청이 우선권을 가져야 할 수도 있습니다. 불행히도 생성 길이를 추정하는 것은 매우 어렵습니다. 일부 요청에는 "max_tokens" 제한이 있지만, 대화형 AI 어시스턴트에서 일반적인 채팅 메시지는 토큰 제한이 없으며, 매우 짧은 생성 ("이 텍스트 요약") 에서 매우 긴 생성 ("이야기 말해" / "xyz 의 모든 코드 작성") 까지 다양합니다.
프롬프트에 따라 요청을 유사성에 기반하여 배열하는 데 일부 이점이 있을 수 있으므로, vLLM 이 캐시 히트를 최대화할 수 있습니다. 이는 성능을 향상시킵니다. NVIDIA Dynamo 와 AIBrix 와 같은 프레임워크에서 최근 이러한 KV-cache-aware 라우팅에 주목받았습니다.
비즈니스 컨텍스트와 호스팅된 LLM 의 경우, 개별 요청의 비용은 고려할 수 있는 또 다른 지표이지만 유사한 도전과제를 가지고 있습니다.
해결책은 사용자 (및 모델) 당 하나의 큐가 아닌, 우선순위가 다른 여러 개의 큐를 갖도록 확장할 수도 있습니다. 예를 들어, 채팅 인터페이스를 갖춘 TNG AI Assistant 같은 상호작용 애플리케이션은 5 초 이내에 진행 상황을 보지 않는 사용자는 애플리케이션이 고장 났다고 생각할 것이므로 더 높은 우선순위를 가져야 합니다. 반면, 수십 개의 파일과 수천 줄의 코드를 생성하는 코드 리뷰를 수행하는 사용자들은 LLM 요청이 다소 시간이 걸릴 것을 기대합니다. 그리고 일부 사용 사례 (예: 배치 API 를 통해 스케줄링되는 벤치마크 실행) 는 다른 사용 사례를 방해하지 않고 아무것도 실행되지 않을 때만 스케줄링되도록 매우 낮은 우선순위를 가져야 합니다.
LLM-Server 측의 스케줄러가 모든 요청을 (공정 우선순위에 따라) 즉시 백엔드로 보낸다고 가정해 봅시다. 사용자 A 가 많은 요청을 동시에 보내고, 얼마 후 새로운 사용자 C 가 단일 요청을 스케줄링하고 싶어 합시다. 모든 사용자 A 의 요청이 다시 FIFO 큐에 쌓여버릴 것입니다. 새로운 사용자 C 는 이전에 받은 모든 요청이 처리될 때까지 기다려야 합니다. LLM-Server 가 없으면 초기 시나리오에는 거의 개선이 없을 것입니다.
이상적으로는 백엔드 큐의 최대 요소 수를 제한할 수 있지만, vLLM 은 이를 허용하지 않습니다. 따라서 우리는 LLM-Server 측의 스케줄러가 백엔드로 새로운 요청을 보내는 속도를 동적으로 조정해야 합니다. 여기서 우리의 목표는 새로운 사용자들이 경험하는 지연 시간을 최소화하기 위해 백엔드 큐 길이를 짧게 유지하는 것입니다.
(가장 간단한 접근 방식은 정적 속도 제한이지만, 대부분의 요청이 짧고 다른 모델과 로드 패턴에 대해 조정하기 어렵습니다.)
LLM-Server 에서 백엔드 큐 길이를 확인하려면 vLLM /metrics 엔드포인트에서 해당 Prometheus 지표를 가져와야 합니다. 우리의 공정 스케줄러는 백엔드 큐 길이 지표가 3 보다 작을 때만 백엔드로 요청을 보낼 수 있습니다. 예를 들어, 새로운 사용자의 지연 시간을 더욱 단축하기 위해 백엔드 큐 길이를 낮출 수 있지만, 이는 과다 활용되지 않는 배치와 효율성 감소를 초래할 때까지입니다 - 여기에는 트레이드오프가 있습니다. 다만, 목표 큐 길이는 최대 동시성을 반영하지 않는다는 점을 기억하세요. 동시에 3 개 이상의 요청이 처리될 수도 있습니다. vLLM 은 충분한 공간이 있을 때 ("연속 배치") 현재 배치에 큐 요청을 추가합니다.
LLM-Server 에서 백엔드 큐와 스케줄러 간의 피드백 루프를 구축하면, 스케줄링 결정에 사용되는 vLLM 지표 집합을 쉽게 확장할 수 있습니다. 예를 들어, TNG 의 상호작용 AI Assistant 를 위한 좋은 사용자 경험을 위해 우리는 높은 토큰 생성 속도 (예: >7 토큰/초, 즉 ~150ms 당 토큰) 를 목표로 하므로, 보고된 토크당 출력 시간 지표가 150ms 를 초과하면 새로운 요청은 스케줄링되지 않습니다.
또한 다른 우선순위의 요청에 대해 다른 지표 임계값을 프로그래밍할 수 있습니다. 예를 들어, 배치 API 에서의 낮은 우선순위 요청은 백엔드 큐가 완전히 비어있을 때만 요청을 스케줄링합니다: 우리는 더 높은 우선순위를 가진 이후 요청의 지연 시간을 증가시키는 것보다 GPU 의 단기 과다 활용 부족을 감수하는 것이 좋습니다.
최적화의 잠재력은 상당합니다: 예를 들어, 세 번째보다 짧은 백엔드 길이를 허용하고 현재 지표가 0 인 경우, 다시 지표를 가져와야 할 때까지 백엔드에 세 개의 요청을 즉시 보낼 수 있습니다. 최악의 경우, 어느 것도 현재 배치에 적합하지 않고 모두 백엔드 큐에 있는 경우 (이때 새로운 길이는 세로, 충분히 좋습니다).
최근 vLLM 은 FIFO 를 유지하기 대신 우선순위 기반 스케줄링 옵션을 추가했습니다. 이 새로운 기능은 요청이 백엔드로 보내지기 전에 우선순위로 태그를 붙입니다. 그리고 vLLM 은 더 높은 우선순위의 요청이 큐에 있는지 정기적으로 확인하고, 모든 요청 (실행 중인 배치와 대기 중인 큐) 을 우선순위에 따라 정렬합니다. 이는 우선순위 요청을 문자 그대로 큐에서 뛰어넘게 하고, 심지어 처리된 배치로 직접 이동하게 할 것입니다. 비용은 (정렬에 의한 일부 오버헤드 제외) 낮은 우선순위의 요청이 실행 중인 배치에서 추방되어 다시 대기 중인 큐로 돌아오게 된다는 점입니다.
vLLM 은 요청 우선순위를 연속적인 숫자로 이해합니다. 이는 기본, 높은 우선순위 (대화형 AI 어시스턴트), 낮은 우선순위 (배치 API) 를 구별하는 것뿐만 아니라, 사용자가 이미 백엔드로 보낸 응답이 대기 중인 모든 요청에 대해 더 작은 감량을 할 수 있습니다. 예를 들어, 사용자 B 와 C 는 단일 요청만 보내며, 이는 우선순위가 0 입니다. 사용자 A 는 네 개의 요청을 한 번에 보내며, 첫 번째는 우선순위가 0 이지만, 다음 ones 은 우선순위가 1, 2, 3 을 가지며, 나중에 처리됩니다 (더 높은 숫자 = 더 낮은 우선순위).
여전히 몇 가지 주의사항이 있습니다:
- 백엔드 우선순위 기능은 vLLM 에만 제공되며, HuggingFace TGI 에는 제공되지 않습니다.
- 스케줄러가 LLM-Server 측에 있으면, 시간당 출력 토큰 (time-per-output-token) 을 기반으로 스케줄링 속도를 조정할 수 있습니다. 백엔드 우선순위 스케줄링은 스케줄링 속도를 제어하지 않습니다.
- 큐와 배치의 빈번한 재정렬이 지연 시간에 미치는 영향은 실제 부하 시나리오에서 측정해야 합니다.
전반적으로, 백엔드 측 우선순위 스케줄링은 vLLM 기반 시스템에 좋은 전략일 수 있습니다. 왜냐하면 이는 상류 LLM-Server 의 큐잉 논리를 단순화하기 때문입니다. 불행히도, 현실적인 설정에서는 개별 요청에 우선순위를 할당하는 객관적인 인스턴스를 필요로 하므로 LLM-Server 를 추가 스케줄링 레이어로 제거할 수 없습니다.
큐잉과 스케줄링은 다양한 우선순위의 사용자 및 클라이언트를 가진 애플리케이션에 매우 중요하며, 이는 사용자 경험에 큰 영향을 미칩니다. 특히 여러 클라이언트가 병렬로 요청을 제출하는 시나리오에서는 전용 "공정한 스케줄링" 전략에서 혜택을 받습니다. 백엔드 기능과 같은 우선순위 스케줄링은 최적화를 단순화할 수 있지만, 이러한 복잡성을 완전히 관리하려면 상류 게이트웨이 서버가 여전히 필요합니다.
이 블로그 포스트 시리즈의 다음 부분에서는 토큰 생성 (token generation) 에 초점을 맞추고, prefill 및 decode 단계 동안 추론 엔진에서 논의합니다. 특히, 자원 활용과 여러 요청의 동시 처리 전략에 대해 논의할 것입니다.