TT Lab
블로그

한국어 English 日本語 中文

시작하기
블로그

블로그

prefill 과 decode 를 나누면, 작은 모델과 느린 망에서 잰 분리 서빙

상태: 초안 (2026-11-14), 실측. 이 글의 숫자는 2026-11-13 에 필자가 직접 잰 값입니다. 환경과 요청은 1편과 같습니다. 쿠버네티스 v1.34, 24GB급 GPU 한 장짜리 서버와 8GB급 노트북 GPU 서버, 노드 사이 1GbE, RDMA 없음, Dynamo v1.6.2 의 vllm-runtime:1.6.2 이미지(vLLM 0.31.0), 모델 Qwen3-0.6B 입니다. 분리 서빙이 불리한 쪽의 조건이라는 점을 먼저 밝힙니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.

분리 서빙을 작은 클러스터에서 재 보기

목차

1. 분리 서빙이 하려는 일

요청 하나를 처리하는 일은 둘로 나뉩니다. 프롬프트 전체를 읽어 KV 를 만드는 일(prefill)과, 그 KV 를 보며 토큰을 하나씩 내는 일(decode)입니다. prefill 은 한 번에 큰 계산을 하고 decode 는 작은 계산을 오래 반복합니다. 둘이 같은 GPU 에서 돌면 긴 prefill 하나가 다른 요청들의 토큰 생성을 멈춰 세웁니다.

분리 서빙은 이 둘을 서로 다른 워커에 맡깁니다. prefill 워커가 KV 를 만들고, 그것을 decode 워커로 옮긴 뒤, decode 워커가 토큰을 냅니다. 값은 KV 를 옮기는 시간입니다. Dynamo 는 이 전송에 NIXL 이라는 라이브러리를 쓰고, 문서는 RDMA 를 전제로 설명합니다. TCP 로도 돌지만 훨씬 느리다고 적혀 있습니다.

이 편이 답하려는 것은 셋입니다.

  1. 쿠버네티스에서 Grove 없이 분리 서빙 그래프가 뜨는가.
  2. RDMA 가 없는 환경에서 같은 서버 안의 분리는 얼마를 치르는가.
  3. 1GbE 를 건너는 분리는 얼마를 치르는가.

2. 구성

DGD 의 구성 요소에 type: prefill 과 type: decode 를 두고, 워커 인자에 역할을 적습니다.

# prefill 워커
- --disaggregation-mode
- prefill
- --kv-transfer-config
- '{"kv_connector":"NixlConnector","kv_role":"kv_producer","kv_buffer_device":"cpu"}'

# decode 워커
- --disaggregation-mode
- decode
- --kv-transfer-config
- '{"kv_connector":"NixlConnector","kv_role":"kv_consumer","kv_buffer_device":"cpu"}'

# 둘 다
env:
  - {name: UCX_TLS, value: "tcp,self"}
  - {name: UCX_RNDV_THRESH, value: "inf"}

RDMA 가 없으므로 전송을 TCP 로 고정했습니다. kv_buffer_device 를 cpu 로 둔 것은 Dynamo 예제의 주석을 따른 것입니다. GPU 버퍼로 TCP 를 타면 UCX 안에서 죽는다고 적혀 있습니다. 이 설정에서는 KV 가 호스트 메모리를 거쳐 갑니다. 워커 로그에도 「UCX CUDA support was not found」라는 경고가 남았습니다.

배치는 둘입니다.

이름 prefill 워커 decode 워커 KV 가 지나는 길
같은 서버 24GB급 GPU 서버에 2대 같은 서버에 2대 한 서버 안의 TCP
서버 사이 8GB급 GPU 서버에 1대 24GB급 GPU 서버에 2대 1GbE

프런트엔드는 3편과 같이 KV 라우팅 모드입니다. 엔진 인자와 요청도 같습니다. 요청 하나는 평균 2,371토큰이고, 토큰 하나의 KV 가 114,688바이트이므로 요청 하나의 KV 는 약 272MB 입니다.

Grove 없이 떴습니다. DGD 를 적용하자 오퍼레이터가 프런트엔드, prefill, decode 를 각각 Deployment 로 만들었고, 2분 안에 다섯 파드가 모두 Ready 가 됐습니다. 프런트엔드는 요청을 prefill 워커에 먼저 보내고 이어서 decode 워커로 넘겼습니다. 차트 기본값에서 Grove 는 꺼져 있고, 이 크기의 그래프에는 필요하지 않았습니다.

3. 같은 서버 안의 분리

접두 2·7·12·24종, 동시 4·8, 점마다 3회의 중앙값입니다. 24개 측정 모두 오류는 0건이었습니다. 견줄 값은 3편의 KV 라우팅(워커 6대)입니다.

접두 종류 동시 TTFT p50: KV 라우팅 / 분리 TTFT p99: KV 라우팅 / 분리 초당 요청: KV 라우팅 / 분리
2 4 34 / 286ms 73 / 592ms 10.9 / 6.5
7 4 40 / 375ms 129 / 1,007ms 8.9 / 5.6
12 4 43 / 465ms 138 / 887ms 8.7 / 5.0
24 4 64 / 518ms 149 / 958ms 8.1 / 4.8
2 8 49 / 462ms 145 / 981ms 14.7 / 9.0
7 8 60 / 650ms 163 / 1,477ms 12.2 / 7.2
12 8 69 / 794ms 172 / 1,744ms 12.1 / 6.4
24 8 106 / 896ms 184 / 1,776ms 11.5 / 6.0

두 구성은 자원이 똑같지 않습니다. KV 라우팅 쪽은 같은 24GB급 GPU 에 워커 4대를 두고 다른 서버의 워커 2대를 더 썼습니다. 분리 쪽은 그 GPU 한 장에 워커 4대(prefill 2, decode 2)만 썼습니다. 그래서 처리량의 차이에는 워커 2대의 몫이 섞여 있습니다. 반면 첫 토큰 시간의 차이는 그것으로 설명되지 않습니다.

가장 유리한 칸을 보면 알 수 있습니다. 접두 2종·동시 4 에서 캐시 토큰 비율은 0.857 로, prefill 워커가 접두를 거의 다시 계산하지 않았습니다. 그런데도 첫 토큰까지 286ms 가 걸렸습니다. KV 라우팅에서 같은 칸은 34ms 였습니다. prefill 워커의 캐시에 맞아도 decode 워커는 그 KV 를 갖고 있지 않으므로 272MB 를 통째로 받아야 합니다. 늘어난 약 250ms 가 그 값입니다. 272MB 를 0.25초에 옮겼다면 초당 1GB 남짓인데, 같은 서버 안에서 호스트 메모리를 거쳐 TCP 로 복사한 속도로는 그럴듯한 크기입니다. 전송만 따로 잰 것은 아니므로 이 분해는 추정입니다.

동시성이 오르면 전송이 겹쳐 더 늘어납니다. 동시 8 에서 중앙값은 0.46~0.9초, p99 는 1~1.8초였습니다.

적중률도 낮아졌습니다. 접두 12종에서 캐시 토큰 비율이 0.40~0.42, 24종에서 0.20 안팎으로, KV 라우팅(0.57~0.66, 0.36~0.43)에 못 미칩니다. 접두 캐시를 쥐고 있는 쪽이 prefill 워커 둘뿐이라, 담을 수 있는 접두의 총량이 워커 6대일 때의 3분의 1이기 때문입니다.

정리하면 이 조건에서 같은 서버 안의 분리는 첫 토큰이 8~12배 늦고 처리량이 37~48% 낮았습니다. GPU 가 한 장이라 계산 자원은 늘지 않았고, prefill 이 100ms 안쪽으로 끝나는 작은 모델이라 「긴 prefill 이 decode 를 막는다」는 문제가 처음부터 작았습니다. 얻을 것이 없는 곳에 전송 비용만 얹은 구성입니다.

4. 서버 사이의 분리: 느렸고, 그 전에 메모리가 먼저 찼다

1GbE 는 실제로 초당 110~117MB 를 나릅니다. 272MB 를 옮기면 2.3~2.5초가 걸린다는 계산이 나옵니다.

이 배치는 끝까지 재지 못했습니다. prefill 워커가 요청 몇 건 만에 OOMKilled 됐기 때문입니다.

시도 성공한 요청 그 요청들의 TTFT 그 뒤
동시 4 2건 2,039ms, 2,040ms 부하를 건 지 9초 만에 prefill 워커 OOMKilled(종료 코드 137), 남은 요청 전부 실패
동시 1 4건 중앙값 1,637ms, 최대 2,653ms 같은 워커가 다시 OOMKilled

성공한 여섯 건의 첫 토큰은 1.6~2.7초로, 링크 속도로 계산한 값과 같은 크기입니다. 표본이 여섯 건뿐이므로 분포를 말할 수는 없습니다. KV 라우팅의 34ms 와 견주면 50배 안팎입니다.

메모리 쪽이 더 쓸모 있는 발견입니다. prefill 워커의 메모리 한도는 3,584Mi 였습니다. 같은 이미지와 같은 엔진 인자로 3편의 모든 측정을 오류 없이 돌린 한도입니다. kv_buffer_device: cpu 에서는 옮길 KV 가 호스트 메모리에 올라가므로 요청 하나에 272MB 가 더 듭니다. 동시 4 면 1GB 를 넘습니다. 동시 1 에서도 네 건 만에 한도를 넘은 것을 보면 전송이 끝난 버퍼가 곧바로 돌아오지 않은 것으로 보입니다. 워커의 메모리 사용량을 시간에 따라 재지는 않았으므로 원인은 미확인입니다.

같은 서버 배치에서는 이 문제가 없었습니다. 그쪽 워커의 한도는 10Gi 였습니다. 서버 사이 배치의 한도를 올려 다시 재지는 않았습니다. 그 서버에 다른 워크로드가 함께 있어서 한도를 넉넉히 줄 수 없었습니다.

여기서 가져갈 것은 하나입니다. 분리 서빙으로 옮기면 워커의 메모리 한도를 다시 계산해야 합니다. GPU 버퍼로 RDMA 를 타는 구성에서는 사정이 다르겠지만(미실측), TCP 와 호스트 버퍼를 쓰는 구성에서는 「동시 요청 수 × 요청 하나의 KV 크기」에 여유를 더한 만큼이 기존 한도 위에 필요합니다.

5. 그러면 언제 이득인가

이 글은 분리 서빙이 이기는 조건을 재지 못했습니다. 잰 것은 지는 조건 둘입니다. 다만 손익을 가르는 양이 무엇인지는 측정에서 드러났습니다.

분리 서빙이 치르는 값 ≈ 요청 하나의 KV 바이트 ÷ 전송 처리량
분리 서빙이 얻는 값 ≈ 긴 prefill 이 같은 GPU 의 decode 를 막던 시간

문서가 RDMA 를 전제로 삼는 까닭이 이 식에 있습니다. 전송 처리량이 수십 GB/s 가 되어야 치르는 값이 prefill 시간보다 작아집니다. RDMA 나 NVLink 가 있는 환경에서의 손익은 미실측입니다.

6. 정리

시리즈 전체를 한 줄씩 줄이면 이렇습니다. Service 뒤의 vLLM 은 접두가 한 대의 용량을 넘으면 캐시를 버립니다(1편). Dynamo 를 들이려면 오퍼레이터의 웹훅 범위와 권한부터 봐야 합니다(2편). KV 라우팅은 그 구간에서 적중을 두세 배로 올립니다(3편). 분리 서빙은 빠른 망과 큰 모델이 있을 때의 이야기입니다(이 편).

참고

로그인하면 좋아요를 누를 수 있습니다

댓글

아직 댓글이 없습니다.

로그인하면 댓글을 쓸 수 있습니다