LLM 요청 하나를 처리하는 일은 성질이 전혀 다른 두 계산으로 나뉜다. 프롬프트 전체를 읽어 KV 캐시를 만드는 프리필(prefill)과, 그 위에서 토큰을 하나씩 뽑는 디코드(decode)다. 보통은 이 둘을 같은 GPU가 이어서 한다.
그런데 두 계산은 병목이 다르다. 하나는 계산이 부족해서 느리고 다른 하나는 메모리 대역폭이 부족해서 느리다. 성질이 다른 일을 한 자원에 섞어 두면 서로를 방해하는데, 그 방해가 지표 한 곳에만 나타나지 않아 원인을 짚기가 어렵다.
두 단계의 병목이 다른 이유
프리필은 프롬프트 토큰 4,000개를 한 번에 넣는다. 층마다 도는 것은 4,000 × 4096 행렬과 가중치 행렬의 곱, 즉 행렬과 행렬의 곱이다.
디코드는 토큰 하나를 넣는다. 같은 층에서 도는 것은 1 × 4096 벡터와 같은 가중치 행렬의 곱, 즉 행렬과 벡터의 곱이다.
가중치를 GPU 메모리에서 읽어 오는 양은 두 경우가 똑같다. 그 가중치로 하는 계산량만 4,000배 차이가 난다. 이 비율을 산술 강도(arithmetic intensity)라고 부른다 — 읽어 온 바이트당 몇 번 계산하는가다.
프리필은 이 값이 크다. 한 번 읽은 가중치로 4,000개 토큰 몫을 계산하니 GPU의 연산 유닛이 꽉 찬다. 디코드는 이 값이 작다. 16GB짜리 가중치를 전부 읽어 놓고 토큰 하나 몫만 계산하고 버린다. 디코드에서 GPU는 대부분의 시간을 계산이 아니라 기다림에 쓴다.
| 프리필 | 디코드 | |
|---|---|---|
| 한 번에 처리하는 토큰 | 프롬프트 전체 | 1개 |
| 연산 모양 | 행렬 × 행렬 | 행렬 × 벡터 |
| 병목 | 연산 유닛 | 메모리 대역폭 |
| 걸리는 시간 | 프롬프트 길이에 비례 | 길이와 거의 무관, 대신 매 토큰 반복 |
| 좌우하는 지표 | 첫 토큰까지의 시간(TTFT) | 토큰 사이 간격(ITL) |
| 배치를 키우면 | 이미 꽉 차서 이득이 적다 | 이득이 크다 — 읽은 가중치를 나눠 쓴다 |
마지막 줄이 특히 중요하다. 디코드는 배치를 키울수록 효율이 좋아지고, 프리필은 그렇지 않다. 하나의 서버가 둘을 다 하면 배치 정책을 어느 쪽에 맞출지 매번 타협해야 한다.
섞여 있을 때 벌어지는 일
연속 배칭 엔진은 걸음마다 배치를 다시 짠다. 그 배치에 새로 들어온 요청의 프리필이 섞이면, 그 걸음은 프리필이 끝날 때까지 길어진다. 같은 배치에 있던 디코드 요청들은 그동안 다음 토큰을 못 낸다.
사용자가 겪는 것은 이렇다. 화면에 글자가 흘러나오다가 잠깐 멈추고, 다시 흐르다가 또 멈춘다. 멈추는 이유는 그 요청과 아무 상관이 없다 — 다른 사람이 긴 프롬프트를 넣었을 뿐이다.
평균 지연은 이 문제를 거의 안 보여 준다. 걸음 대부분은 정상이고 프리필이 낀 걸음만 길기 때문에, p50은 멀쩡하고 p99만 나빠진다. 벤치마크에서 토큰 사이 간격의 분포를 따로 보지 않으면 지나치기 쉽다.
완화책이 하나 있다. 청크 프리필(chunked prefill)은 긴 프리필을 512토큰 같은 조각으로 쪼개 여러 걸음에 나눠 넣는다. 한 걸음이 길어지는 정도가 줄어드니 토큰 사이 간격의 튐이 확실히 작아진다. vLLM과 SGLang 모두 기본으로 켜 두는 쪽에 가깝고, 대부분의 서비스는 여기까지로 충분하다.
다만 청크 프리필이 없애는 것은 튐의 크기이지 자원 경쟁 자체가 아니다. 프리필 조각이 걸음마다 조금씩 끼어들면 디코드의 처리량은 계속 갉아먹히고, 반대로 프리필 쪽에서 보면 조각으로 쪼갠 만큼 행렬이 작아져 프리필 자체의 효율도 조금 떨어진다. 그리고 배치 정책·병렬화 방식을 두 단계가 여전히 공유해야 한다는 사실은 그대로다.
나누면 무엇이 달라지는가
분리 서빙(disaggregated serving)은 이 타협을 구조로 없앤다. GPU 무리를 둘로 나눠 한쪽은 프리필만, 다른 쪽은 디코드만 맡게 한다.
요청은 프리필 노드에 들어가 KV 캐시가 되고, 그 KV 캐시가 디코드 노드로 옮겨진 뒤 거기서 토큰이 나온다. 얻는 것은 셋이다.
- 간섭이 사라진다. 디코드 노드에는 프리필이 아예 들어오지 않으므로 토큰 사이 간격이 고르다. 프리필 노드에는 디코드가 없으므로 큰 행렬 곱을 방해 없이 돌린다
- 배치 정책을 따로 정한다. 디코드 쪽은 배치를 크게 잡아 처리량을 올리고, 프리필 쪽은 첫 토큰까지의 시간에 맞춰 잡는다. 서로 다른 목표를 서로 다른 손잡이로 맞춘다
- 병렬화와 하드웨어를 따로 고른다. 디코드는 메모리 대역폭이 병목이니 대역폭 좋은 카드가, 프리필은 연산이 병목이니 연산 성능 좋은 카드가 유리하다. 텐서 병렬 정도도 두 무리에서 다르게 잡을 수 있다
세 번째가 규모가 커질수록 값어치가 커진다. 한 서버에 묶여 있으면 두 단계가 같은 카드, 같은 병렬 구성을 쓸 수밖에 없다.
새로 생기는 비용 — KV 캐시를 옮긴다
공짜가 아니다. 이 구조는 프리필이 만든 KV 캐시를 네트워크로 디코드 노드에 보내야 한다. 그 크기를 세어 보면 도입 가능 여부가 거의 정해진다.
토큰 하나가 층마다 차지하는 KV 캐시는 이만큼이다.
키와 값 둘이므로 2, 는 KV 헤드 수, 는 헤드 차원, 는 자료형 바이트 수다. Llama 3.1 8B는 GQA를 써서 KV 헤드가 8개이고 헤드 차원이 128, fp16이면 2바이트다.
층이 32개이므로 토큰당 128KB다. 프롬프트 4,000토큰이면 약 500MB를 옮겨야 한다. 이 500MB가 얼마나 걸리는지가 전부다.
| 연결 | 대역폭 | 500MB 전송 시간 |
|---|---|---|
| NVLink (노드 안) | 900GB/s | 0.6ms |
| InfiniBand · RDMA 400Gb/s | 50GB/s | 10ms |
| 일반 이더넷 25Gb/s | 3.1GB/s | 160ms |
맨 아래 줄이면 이 구조는 성립하지 않는다. 프리필이 200ms 걸리는데 전송이 160ms면 첫 토큰까지의 시간이 거의 두 배가 된다. 분리 서빙은 사실상 고속 인터커넥트를 전제로 하는 구조다.
전송 시간을 줄이는 기법들은 대체로 「끝날 때까지 기다리지 않는다」는 한 방향이다. 층별로 KV가 완성되는 대로 먼저 보내 프리필 계산과 전송을 겹치거나, 디코드 노드가 앞쪽 층부터 받아 시작하는 식이다. 잘 겹치면 전송 시간의 상당 부분이 프리필 시간 뒤에 숨는다.
그리고 KV 캐시를 여러 노드가 주고받게 되면 그것을 어디에 어떻게 두는가가 별도의 문제가 된다. PagedAttention처럼 KV를 블록 단위로 다루는 구조가 여기서 다시 쓰인다 — 블록이면 부분 전송과 재사용이 자연스럽다.
두 무리의 비율
나누고 나면 새 손잡이가 하나 생긴다. 프리필 노드와 디코드 노드를 몇 대 몇으로 둘 것인가.
이 비율은 트래픽의 입출력 길이 비율이 정한다. 대략 이렇게 갈린다.
- 입력이 길고 출력이 짧으면(문서 요약, 분류, 추출) 일이 프리필 쪽에 쏠린다. 프리필 노드를 늘린다
- 입력이 짧고 출력이 길면(대화, 코드 생성, 긴 추론) 디코드 쪽에 쏠린다. 디코드 노드를 늘린다
문제는 이 비율이 시간대에 따라 바뀐다는 것이다. 낮에는 요약 트래픽이, 밤에는 배치 생성이 몰리는 식이면 고정 비율은 어느 쪽이든 놀린다. 그래서 실제 운영에서는 두 무리를 각각 오토스케일링 대상으로 두고 큐 길이에 따라 따로 늘린다.
여기서 분리 서빙이 만든 새 실패 모드가 하나 나온다. 한쪽만 막히면 다른 쪽은 놀면서 전체가 느려진다. 프리필 노드가 부족하면 디코드 GPU는 한가한데 대기열은 길어진다. 합쳐 뒀을 때는 자원이 알아서 섞이던 것이 이제 안 섞이므로, 두 무리의 대기열을 각각 보고 있어야 한다.
쓸 값어치가 있는가
정리하면 이렇다.
| 상황 | 권하는 것 |
|---|---|
| GPU 몇 장, 트래픽 중간 | 청크 프리필로 충분하다 |
| 인터커넥트가 일반 이더넷 | 분리하지 않는다 — 전송이 이득을 먹는다 |
| 토큰 사이 간격 p99가 목표를 못 맞춘다 | 먼저 청크 프리필, 그래도 안 되면 분리 |
| 입출력 길이가 극단적으로 치우쳐 있다 | 분리의 이득이 크다 |
| GPU 수십 장 이상, RDMA 있음 | 분리를 검토할 자리 |
대부분의 서비스는 첫 줄이다. 분리 서빙은 노드 사이 전송 경로, 두 무리의 스케줄러, 각각의 오토스케일링, 그리고 한쪽만 막히는 상황의 관측까지 만들어야 하는 구조라 운영 복잡도가 눈에 띄게 올라간다. 그 복잡도를 감당할 값어치는 규모가 어느 선을 넘어야 생긴다.
그래도 이 구조를 알아 둘 값어치는 있다. 큰 추론 서비스들이 이 방향으로 움직이고 있고, 무엇보다 프리필과 디코드가 서로 다른 자원을 다투는 두 계산이라는 사실 자체가 분리를 안 하더라도 서빙 지표를 읽는 방식을 바꿔 놓기 때문이다. 첫 토큰까지의 시간과 토큰 사이 간격을 하나의 「지연」으로 묶어 보고 있었다면, 그 둘은 원래 다른 손잡이로 움직이는 다른 숫자다.
정리
- 프리필은 연산이 병목이고 디코드는 메모리 대역폭이 병목이다. 한 GPU에 섞으면 배치 정책부터 하드웨어 선택까지 매번 타협하게 된다
- 섞여 있을 때의 증상은 토큰 사이 간격의 튐이고, 평균이 아니라 p99에만 나타난다
- 청크 프리필이 튐의 크기를 줄여 준다. 대부분의 서비스는 여기까지로 충분하다
- 분리 서빙은 간섭을 구조로 없애는 대신 KV 캐시 전송이라는 새 비용을 만든다. 8B 모델에 4,000토큰이면 500MB이고, 이것이 몇 ms인지가 도입 가능 여부를 정한다
읽어주셔서 감사합니다. 😊

