LabHub
시작하기
배우기 러닝패스 코스

LLM 서빙

KV 인식 라우팅과 분리 서빙: 요청을 어디로 보낼 것인가

LabHub 에서 이어서 보기

한 줄 요약

서버가 여러 대일 때 요청을 아무 서버에나 보내면 이미 계산해 둔 KV 캐시를 버리게 된다. NVIDIA Dynamo 의 KV 인식 라우터는 요청의 앞부분이 가장 많이 겹치는 워커로 보내고, 이 측정에서는 같은 접두사를 다시 보낸 60번이 거의 모두 캐시에 적중했다(이벤트 방식은 여섯 번 모두 60번, 근사 방식은 60, 53, 60번이었고 라운드 로빈은 35–43번). 반대로 프리필과 디코드를 다른 프로세스로 나누는 분리 서빙은 같은 GPU 한 장에서 첫 토큰이 4.7–7.2배 느려졌다. KV 를 옮기는 값을 치르기 때문이다.

왜 이게 필요했나

LLM 서버는 같은 앞부분(접두사)을 가진 요청의 KV 캐시를 재사용한다. 시스템 프롬프트나 긴 문서를 앞에 붙이고 질문만 바꾸는 요청이라면 앞부분의 계산을 건너뛸 수 있다. 문제는 서버가 한 대가 아닐 때다. 워커가 둘이고 요청을 번갈아(라운드 로빈) 보내면, 어제 워커 0 이 계산해 둔 문서가 오늘은 워커 1 로 가서 다시 계산된다. 캐시는 워커 안에 있고, 요청을 나누는 쪽은 그것을 모르기 때문이다.

두 번째 질문은 일의 성격이 다른 두 단계를 한 프로세스에 둘 것인가다. 프롬프트를 읽는 프리필은 계산이 몰리고, 토큰을 하나씩 내는 디코드는 메모리를 읽는 일이 많다. 둘을 같은 GPU 에서 섞으면 긴 프리필이 들어올 때 다른 요청의 토큰 간격이 늘어난다. 그래서 프리필 전용 워커와 디코드 전용 워커를 따로 두고 KV 를 옮기는 분리 서빙이 나왔다. 이 모듈은 두 질문을 직접 만든 시뮬레이터와 실측으로 본다.

어떻게 동작하나

Dynamo 는 요청을 받는 프런트엔드(HTTP, 토큰화, 라우팅)와 모델을 돌리는 워커를 나눈다. 프런트엔드는 워커를 발견(discovery)으로 찾는다. 쿠버네티스에서는 쿠버네티스 자체를 쓰고, 이 측정에서는 파일 하나로 대신했다.

라우터의 아이디어는 세 줄이다.

  1. 프롬프트를 일정한 크기(이 실습은 16 토큰)의 블록으로 나누고, 블록마다 앞에서부터 그 블록까지 전체를 가리키는 이름(해시)을 붙인다. 앞 블록이 다르면 같은 내용의 블록도 다른 이름이 된다.
  2. 워커마다 어떤 블록 이름을 가지고 있는지 색인을 둔다. 요청의 블록을 앞에서부터 세어 이어서 가진 만큼이 그 워커의 겹침이다.
  3. 겹침이 가장 큰 워커로 보낸다. 비기면 일이 적은 쪽이다.

색인을 채우는 방법이 둘이다. 하나는 라우터가 자기가 보낸 곳을 기억하는 근사 방식이다. 다른 하나는 워커가 KV 이벤트로 "이 블록을 가지고 있다" 고 알려 주는 방식이다. 두 번째가 정확하지만 워커가 이벤트를 내보내야 한다. 라우터를 이벤트 모드로 켰는데 워커가 이벤트를 내보내지 않으면 색인이 비어 있고, 그 순간 라우터는 로그에 경고를 하나 남기고 조용히 부하 분산만 하는 라우터가 된다. 실습의 5단계가 이 함정을 직접 재현한다.

분리 서빙에서는 프리필 워커가 계산한 KV 를 디코드 워커가 NIXL(전송 라이브러리)로 받는다. 그래서 첫 토큰이 나오기 전에 KV 를 옮기는 시간이 더해진다.

실측 결과

환경은 8GB급 GPU 한 장, 모델은 Qwen3-0.6B, NVIDIA Dynamo 1.6.0.dev20261006(개발 빌드), vLLM 0.30.0, NIXL 1.3.2 이고, 요청은 모두 한 번에 하나씩 보냈다(분리 서빙 표만 동시성 4 칸이 있다). 원자료와 스크립트는 docs/experiments/2026-10-07-nvidia-dynamo/ 에 있다.

프런트엔드를 거치는 비용. 같은 모델을 vLLM 만 띄운 경우와 Dynamo 프런트엔드 뒤에 둔 경우를 입력 512 토큰, 출력 128 토큰으로 세 번씩 비교했다. 동시성 1 에서 첫 토큰 시간의 가운데 값이 29–39 ms 에서 42–43 ms 로 약 13 ms 늘었다. 동시성 8 과 32 에서는 130 ms 대 129–141 ms, 272 ms 대 272–274 ms 로 같았고 처리량도 같았다(초당 1,507 대 1,509 토큰, 동시성 32). 프런트엔드의 비용은 서버가 한가할 때 보이는 고정 비용이고, 바쁘면 묻힌다.

라우터. 워커 둘(각각 KV 자리 약 14,800 토큰), 접두사 12종을 각 6번씩 섞어 보냈다(요청 72개, 접두사를 두 번째 이후로 보낸 재요청 60개, 접두사는 약 1,500 단어). 재요청 중 첫 토큰 시간이 처음 보낸 요청의 60% 미만이면 캐시에 적중한 것으로 보았다(적중하면 약 15 ms, 아니면 약 80 ms 였다).

라우팅 재요청 60번 중 적중(세 번) 재요청 첫 토큰 평균
라운드 로빈 35, 40, 43 34–42 ms
무작위 44, 31, 45 32–46 ms
KV 인식, 근사(스스로 기억) 60, 53, 60 15–26 ms
KV 인식, 이벤트 모드인데 워커가 이벤트를 내보내지 않음 39, 44, 41 32–38 ms
KV 인식, 이벤트 모드이고 워커가 이벤트를 내보냄 60, 60, 60 15 ms

같은 구성을 앞서 한 번 더 돌린 결과도 같은 방향이었다(라운드 로빈 35, 40, 43, 무작위 37, 37, 41, 근사 60, 60, 60, 이벤트를 내보내지 않음 41, 44, 37, 이벤트를 내보냄 60, 60, 60). 근사 방식은 첫 실행에서 세 번 모두 60번이었는데 이번에는 한 번이 53번이어서, 스스로 기억하는 방식이 이벤트 방식보다 조금 덜 안정할 수 있다고 보지만 두 번의 세 번 반복으로 확정하지는 못한다. 이벤트 방식은 두 번 모두 여섯 번이 전부 60번이었다.

KV 인식 라우팅은 두 방식 모두 거의 모든 재요청을 적중시켰고, 이벤트를 내보내지 않는 구성은 무작위와 구별되지 않았다. 마지막 두 줄이 5단계의 교훈이다. 같은 --router-mode kv 인데 워커 쪽 설정 하나로 결과가 갈렸고, 어느 쪽도 오류를 내지 않았다. 프런트엔드 로그에는 경고 한 줄(kv_event_publisher_disabled, 이 역할이 캐시 인식 라우팅에는 KV 이벤트가 필요한데 발행이 꺼져 있다는 뜻)이 있었고, 이벤트를 내보내는 워커로 바꾼 실행에는 없었다.

실습의 시뮬레이터는 이 표를 방향까지 같게 만든다(KV 인식 60 중 60, 라운드 로빈과 무작위는 그보다 적음). 다만 숫자는 다르다. 시뮬레이터의 라운드 로빈은 60 중 49 인데 실측은 35–43 이다. 시뮬레이터는 워커가 한 번 처리한 접두사를 영원히 기억하지만, 실제 워커의 KV 자리는 약 14,800 토큰이라 접두사 12종(합계 약 18,000 토큰)을 다 담지 못하고 오래된 것부터 밀려난다. KV 인식 라우팅은 접두사를 워커에 나눠 두어 각 워커가 접두사 6종(약 9,000 토큰)만 담으므로 밀려나지 않았다. 라우팅이 적중을 높이는 이유가 하나 더 있다는 뜻이고, 이 분량은 시뮬레이터가 그리지 못한다.

분리 서빙. 같은 GPU 한 장에 프리필 워커와 디코드 워커를 프로세스 둘로 올리고(각각 GPU 메모리의 38%), 한 프로세스가 둘 다 하는 구성과 같은 부하로 견주었다. 출력은 64 토큰이고 값은 반복 세 번의 가운데 값이다.

입력 동시성 한 프로세스(ms) 분리(ms) 배율
1,500 1 78 406 5.2
1,500 4 190 1,359 7.2
3,000 1 171 812 4.7
3,000 4 398 2,442 6.1

분리하면 모든 칸에서 첫 토큰이 4.7–7.2배 느렸다. 워커의 KV 전송 지표로는 요청 하나를 옮기는 데 평균 196 ms(동시성 1, 전송당 119.3 MB, 처리량 608 MB/s)였고, 동시성 4 에서 입력이 3,000 이면 평균 711–1,723 ms 였다. 이 환경에서 분리 서빙이 얻을 것은 없었다. 두 워커가 한 GPU 를 나눠 쓰니 계산 자원이 늘지 않는데 KV 이동 비용이 더해졌고, 전송이 같은 장치 안에서 이루어졌는데도 608 MB/s 에 그쳤다.

전송이 느린 이유는 확정하지 못했다. 전송 방식을 바꿔 보았다. UCX 가 쓸 전송을 TCP 계열로 제한한 설정은 처리량이 621 MB/s, 디버그 로그를 켠 기본 설정은 615 MB/s 로 기본 설정(608 MB/s)과 같았고, TCP 와 같은 장치 안의 IPC 전송(cuda_ipc)을 함께 허용해도 첫 토큰이 같았다. IPC 전송만 허용하면 NIXL 의 UCX 백엔드가 "능동 메시지를 보낼 전송이 없다" 는 오류로 엔진을 시작하지 못했다. 그러니 전송 방식을 고르는 것으로는 이 속도가 바뀌지 않았고, 병목이 어디인지는 미실측 이다.

이 숫자를 어디까지 믿을 것인가

현장에서 만나는 모습

KV 인식 라우팅은 접두사가 길고 자주 다시 쓰이는 서비스(시스템 프롬프트, 긴 문서 질의, 에이전트의 반복 맥락)에서 이득이 크다. 라우터가 이벤트 모드일 때는 워커가 이벤트를 내보내는지부터 확인한다. 이 측정에서는 확인하지 않으면 적중률이 무작위 수준으로 내려가도 겉으로는 정상이었다. 분리 서빙은 GPU 가 둘 이상이고 KV 를 옮기는 연결(NVLink 나 RDMA)이 충분히 빠를 때 검토할 대상이다. 7단계의 계산이 보여 주듯, 연결이 느리면 전송이 프리필 자체보다 오래 걸린다.

다음 실습에서 할 것

GPU 없이 순수 파이썬으로 이 흐름을 만든다. 프롬프트를 블록 이름으로 나누고, 앞에서부터 이어서 겹치는 블록을 세고, 가장 많이 겹치는 워커로 보내는 라우터를 만든다. 라운드 로빈, 무작위, KV 인식 세 전략의 적중률을 비교한 뒤, 워커가 이벤트를 알리지 않으면 라우터가 무엇으로 되돌아가는지 확인한다. 그다음 KV 의 크기와 전송 시간을 계산하는 함수를 만들어 대역폭에 따라 달라지는 비용을 보고, 마지막으로 위 분리 서빙 표의 원본인 측정 파일에서 배율을 직접 계산한다.