분리형 프리필 및 디코드

장치 전반에 걸쳐 프리필과 디코드를 분리하면 LLM 추론 속도가 빨라지고 성능이 향상됩니다.

작성자Perplexity Research

입력 프롬프트에서 출력 토큰을 생성하기 위해 LLM 추론은 프리필(prefill)과 디코드(decode)라는 두 단계로 나뉩니다. 프리필은 입력 토큰을 실행하여 KV 캐시를 채운 다음, 토큰을 하나씩 생성하는 디코드 단계로 넘어갑니다. 일반적으로 단일 디코드 단계는 수십 밀리초 동안 실행되지만, 프리필은 훨씬 더 오랜 시간이 걸립니다. 동일한 장치에서 실행할 경우, 프리필과 디코드를 혼합하면 디코드 성능이 저하됩니다. 이 글에서는 프리필과 디코드를 분리하여 별도의 장치에서 실행함으로써 프리필 처리량과 디코드 지연 시간을 모두 극대화하는 검증된 솔루션을 살펴봅니다.

프리필 대 디코드 성능

일반적인 LLM 서빙 엔진에서는 배치 스케줄러가 모델의 각 실행 단계에서 처리할 요청을 선택합니다. 단일 디바이스 또는 노드에서 실행할 때는 prefill 요청과 decode 요청이 함께 배치됩니다. 시퀀스 길이를 따라 집계되는 attention의 비용은 KV 캐시에 있는 항목의 길이(kv_len)에 비례하여 prefill과 decode 모두에서 증가합니다. Decode 요청은 보통 단일 토큰(qo_len=1)만 전송하며, 시퀀스의 토큰에 독립적으로 동작하는 다른 레이어에서는 최소한의 비용만 발생합니다. Prefill 요청은 수천 개 또는 수만 개의 토큰을 대형 dense 레이어(qo_len)를 통해 전송하므로 상당한 비용이 듭니다.

전방 패스의 지연 시간은 attention 동안 KV 캐시에서 검색되는 토큰 수(kv_len)보다 dense 레이어를 통과하는 독립 토큰 수(qo_len)의 영향을 더 크게 받습니다. Attention은 요청 수와 시퀀스 길이에 비례하는 kv_len 모두에 걸쳐 병렬화할 수 있어 높은 활용도를 달성합니다. Prefill은 연산 집약적입니다. qo_len이 높기 때문에 GEMM 커널은 M 차원에 따라 충분한 블록을 할당하여 최신 GPU의 연산 성능을 완전히 활용할 수 있습니다. Decode는 메모리 병목입니다. 일반적으로 배치 크기가 낮아 M 방향의 입력 수가 작고, 보통 하나의 블록만 충분합니다. Split-K GEMM 커널은 토큰 배치 크기가 낮을 때 SM 활용도를 개선할 수 있지만, 캐시와 행렬 곱셈 유닛은 보통 여전히 충분히 활용되지 못합니다.

트랜스포머 계층을 통한 요청 배칭으로, 쿼리와 키/값 프로젝션, 어텐션, 그리고 KV 캐시 업데이트를 보여줍니다.

프리필 요청이 포함된 배치를 함께 혼합하면 순방향 패스 과정에서 더 높은 지연 시간이 발생하여 전체 인스턴스의 디코드 처리량에 부정적인 영향을 미칩니다. 프리필 요청을 디코드 요청과 혼합하거나 청크 방식의 프리필을 도입하면 디코드 성능이 약간 개선될 수는 있으나, 디코드 처리량을 극대화하기 위해 인스턴스에서 충분한 요청을 처리할 수 있을 만큼 프리필 처리량을 유지하기는 어렵습니다. 대형 모델의 경우 일반적인 출력 길이에서 디코드의 대규모 배치 크기를 유지하려면 프리필을 자주 수행해야 하며, 이는 평균 지연 시간을 크게 저하시키고 출력 끊김 현상을 유발합니다.

세 개의 요청에 대한 혼합 프리필-디코드 스케줄링으로, 공유 타임라인에서 프리필 및 디코드 간격을 보여줍니다.
전용 프리필러가 세 개의 요청에 대한 디코드 작업을 공급하는 분리형 프리필-디코드 스케줄링입니다.

이러한 문제는 prefill과 decode를 수행하는 별도의 노드 집합을 사용하면 해결할 수 있습니다. Prefiller 노드를 여러 decoder 노드와 연결하면, prefill에 충분한 요청을 스케줄링하여 처리량을 극대화할 수 있고, decoder 노드에서도 충분히 큰 동시 요청 수를 유지하여 decode 처리량 역시 극대화할 수 있습니다. Prefiller 노드는 KV 캐시를 채우고, 이후 이를 decoder 노드로 전송합니다. 더 이상 decoder가 prefill 때문에 멈출 필요가 없으므로, 활성 요청의 증가하는 kv_len이 전체에 미치는 영향이 훨씬 덜 두드러져 지연 시간은 더욱 결정적으로 됩니다. 그 대가로 최초 토큰까지의 시간(Time to First Token, TTFT)이 증가하는데, KV 캐시를 네트워크로 전송하는 데 수십 밀리초에서 수백 밀리초가 걸릴 수 있기 때문입니다.

KV 메신저

Perplexity에서 분리형 프리필 및 디코드 구현은 LLM 엔진과 상호 작용하여 네트워크를 통해 프리필러 노드에서 디코더 노드로 KV 캐시 전송을 조정하는 KV 메신저를 중심으로 구축됩니다. 프리필러 측에서 메신저는 디코더 노드의 요청을 수락하여 배치 스케줄러에 전달하고, 순방향 패스 실행을 추적하여 가능한 한 지연 시간을 최소화하며 KV 캐시를 전달합니다. 디코더 측에서는 제거 불가능한 페이지가 할당된 후, 메신저가 KV 캐시 및 디코더 컨텍스트 전송 완료 알림을 받을 때까지 해당 요청의 디코드 스케줄링을 차단합니다.

디코더와 프리필러 간의 조정 및 KV 캐시 데이터 전송을 보여주는 분리형 요청 라이프사이클입니다.

Prefill을 분리하려면 처리량이 높고 지연 시간이 짧은 연결이 필요하므로, 저희 구현은 RDMA에 맞게 설계되어 있으며 EFA와 ConnectX 네트워크 인터페이스 컨트롤러(NIC) 둘 다를 지원합니다. KV Messenger는 libfabric 위에 구축되어 있으며, 저희의 fabric-lib 래퍼를 사용해 Remote Direct Memory Access(RDMA) 프리미티브 위에 더 높은 수준의 저지연 추상화를 제공하고, 효율적인 페이지 및 메타데이터 전송과 저지연 신호 전달을 구현합니다. 백그라운드에서 fabric-lib는 GPU와 직접 연결된 NIC를 조율하여 prefiller 노드에서 decoder 노드로 데이터를 복사합니다.

수신되면 prefiller 노드는 해당하는 소스 KV 페이지 집합을 할당하고, 로컬 엔진을 사용해 요청을 prefill로 스케줄링합니다. 지연 시간을 최소화하기 위해 전송은 전방 패스를 기다리지 않습니다. 대신 모델이 개별 레이어의 KV 캐시에 KV 캐시 항목을 추가하는 즉시 KV 페이지 복사가 시작됩니다. Prefill 요청은 청크 단위로 처리될 수 있으므로, batch scheduler는 실행 전에 현재 스케줄된 청크를 KV messenger에 알립니다. CUDA 그래프를 지원하면서도 레이어를 추적할 수 있도록, messenger는 attention의 output projection 이후 증가하는 카운터를 폴링하는 전용 스레드를 유지합니다. 이 카운터는 샤딩된 환경에서는 리더 노드에만 유지됩니다. KV 캐시 항목은 append 이후 attention 이전에 유효하지만, output projection은 rank 전반에 걸쳐 감소되므로 암묵적으로 이를 동기화하기 때문입니다. 카운터의 변화를 감지하면 messenger에 알림이 전달되고, messenger는 fabric-lib를 호출해 레이어 전송을 시작합니다.

계층별 KV 캐시 전송으로, 프리필러와 디코더 간의 페이징된 쓰기와 단일 쓰기, 그리고 완료 카운터를 보여줍니다.

마지막 청크 전송이 완료된 후 추가 메타데이터도 복사됩니다. 추측 디코딩(speculative decoding) 또는 MTP는 로짓(logits)과 히든 스테이트(hidden states)를 디코더로 이동해야 합니다. 이러한 복사 작업 또한 RDMA를 통해 미리 할당된 버퍼로/로부터 수행됩니다.

마지막 청크의 모든 대기 중 전송이 완료되면 prefiller 노드는 KV 페이지의 할당을 해제하고 요청을 완료합니다. Decoder 노드는 명시적으로 알림을 받지 않습니다. 대신 즉시 카운터를 사용해 완료된 작업 수를 추적합니다. Prefiller 측의 RDMA 작업 수는 전송된 페이지 수에 비례합니다. 알려진 수의 페이지 및 context 복사가 완료되면, fabric-lib는 KV messenger를 호출해 요청이 decode할 준비가 되었음을 표시합니다. Messenger는 남아 있는 context를 할당 해제하고 요청을 LLM 엔진에 넘깁니다.

샤딩된 KV 캐시 전송

Prefiller와 decoder가 Tensor Parallelism(TP)에 의존하고 KV 캐시를 동일하게 샤딩하거나 복제한다면, 단일 transfer engine이 여러 디바이스를 조율하여 모든 복제본의 페이지를 송수신합니다. 모델 실행기가 여러 디바이스와 프로세스에 걸쳐 복제되어 있음에도 단일 messenger와 transfer engine을 사용할 수 있도록, cuMem과 cuMemImportFromShareableHandle을 사용해 KV 캐시를 뒷받침하는 디바이스 메모리를 할당하고 이를 메인 프로세스에 매핑합니다. Transfer engine은 노드의 토폴로지를 검사해 각 KV 캐시 슬라이스 전송에 사용할 NIC와 가장 가까운 NUMA 노드의 CPU를 찾습니다.

소스와 대상이 동일하게 샤딩되는 경우 소스와 대상의 장치 및 페이지 간에 1:1 매핑이 존재하므로 전송은 간단합니다. 이 상황에서 샤딩은 암시적으로 전송 지연 시간을 줄이는 데 도움이 됩니다. 더 많은 GPU를 사용함으로써 더 많은 관련 NIC를 활용할 수 있어 전체 대역폭 활용도에 더 근접할 수 있기 때문입니다. 그러나 불일치가 발생할 경우 전송 엔진은 소스 슬라이스와 대상 슬라이스 간의 비율에 따라 페이지를 분할하거나 재구성해야 합니다.

TP/EP 프리필러와 DP/EP 디코더 간의 KV 캐시 전송 토폴로지로, NIC 연결을 보여줍니다.

프리필러가 더 많은 장치에 걸쳐 KV 캐시를 분할하면 프리필러 장치에서 해당하는 절반을 보내 디코더에서 전체 페이지를 재구성합니다. 디코더에 더 많은 샤드가 있는 경우 여러 소스로부터 페이지를 수신합니다. 디코더는 예상되는 RDMA 쓰기 횟수를 계산할 수 있도록 프리필러의 샤딩 방식을 알아야 합니다. 복제가 포함된 경우 프리필러는 장치를 그 자체 내에서 전체 KV 캐시를 복제하는 복제본 세트로 그룹화합니다. 대상 복제본 세트는 모든 가용한 장치를 사용하여 RDMA 쓰기를 시작하기 위해 소스 세트 중 하나를 무작위로 할당받습니다.

NHD 및 HND KV 캐시 레이아웃으로, 각 계층과 페이지 내에서 토큰과 헤드의 순서를 대비해 보여줍니다.

샤딩된 전송에는 KV 캐시에 약간의 조정이 필요합니다. 기본적으로 FlashInfer는 페이지 내 토큰을 heads 순서로 배치하는 NHD 레이아웃에 의존합니다. 캐시는 대개 attention head 수를 따라 샤딩되므로, 이는 head 내부에 불연속성을 만듭니다. RDMA 전송은 strided write를 암묵적으로 지원하지 않기 때문에 전송을 수행하려면 head마다 하나의 작업이 필요합니다. 대신 libfabric와의 상호작용 수를 줄이기 위해, head 차원을 token 수보다 앞에 두는 HND 레이아웃으로 KV 캐시를 구성합니다. 이렇게 하면 연속성이 보장되어 한 번의 write로 페이지를 복사할 수 있습니다.

추측 디코딩

추측 디코딩은 분리형 프리필-디코드 방식에 약간의 수정을 요구합니다. 당사의 구현에서 프리필러 노드는 토큰을 샘플링할 수 없습니다. Perplexity의 Sonar 모델은 구조화된 출력을 지원하므로 프리필러와 디코더 간에 스키마 프로세서 구현을 동기화하는 복잡성을 초래하고 싶지 않습니다. MTP 및 추측 디코딩 메커니즘에서 마지막 토큰까지 드래프트 모델을 프리필하는 것은 대상 모델에서 토큰을 샘플링하는 것을 포함합니다.

프리필러와 디코더 전반의 추론적 디코딩으로, KV 캐시 길이, 로짓, 그리고 토큰 샘플링을 보여줍니다.

이러한 문제를 해결하기 위해 프리필에는 입력 시퀀스의 마지막 토큰이 포함되지 않습니다. 대신 마지막 토큰 이전의 프리필에서 발생한 히든 스테이트나 로짓이 전송되며, 디코더의 다음 단계에서 디코드 토큰으로 취급됩니다. 첫 번째 토큰을 발행하려면 프리필 후 전체 디코드 단계를 수행해야 하므로 지연 시간이 약간 증가하지만, 구현의 복잡성은 크게 줄어듭니다.

분리형 배포

당사는 프로덕션 트래픽 또는 사내 평가 워크로드를 지원하기 위해 다양한 모델을 사용하여 여러 분리형 구성을 배포하거나 실험했습니다. 모델의 크기와 어텐션 메커니즘에 따라 GPU를 최적으로 활용하기 위해 프리필러 및 디코더 노드에 적합한 샤딩 방식을 선택했습니다.

DeepSeek-R1

DeepSeek의 경우 텐서 병렬(TP) 및 데이터 병렬(DP) 배포를 모두 고려했습니다. 이전 블로그 게시물에서 논의한 바와 같이, TP 배포는 더 나은 지연 시간을 제공하지만 처리량이 낮아 트래픽이 많은 서비스를 처리하기 위해 더 많은 GPU가 필요합니다. DP 배포는 부하에 따라 훨씬 더 잘 확장되지만 장치 간 또는 노드 간 통신 비용으로 인해 최대 처리량은 더 낮습니다.

DeepSeek는 KV 캐시를 압축하는 Multi-Head Latent Attention에 의존합니다. 모든 KV 헤드가 단일 잠재 벡터(latent vector)로 압축되므로, 모든 랭크에서 잠재 벡터를 복제해야 하므로 TP는 KV 캐시를 샤딩할 수 없습니다. 샤딩은 압축 해제 후에 발생하며, 각 랭크는 동일한 잠재 표현에서 서로 다른 헤드를 추출할 수 있습니다. 결과적으로 모든 KV 캐시 샤드는 프리필러 샤드와 디코더 샤드 전체에서 동일합니다.

노드 내 TP 설정에서는 프리필러와 디코더가 동일하게 샤딩됩니다. 가용한 모든 NIC를 완전히 활용하기 위해 모든 랭크에서 전송이 발송됩니다. 그러나 TP 랭크 크기가 낮거나 각 DP 랭크가 단일 GPU에 할당되는 DP 배포의 경우, KV 캐시의 복제본을 보유한 모든 프리필러 장치가 이를 발송할 수 있습니다. 가용한 모든 NIC에 걸쳐 요청의 균형을 맞추기 위해 무작위로 GPU와 NIC를 선택하여 프리필러에서 디코더로 KV 캐시를 전송합니다.

혼합 프리필-디코드 환경에서 R1 배포는 수백 밀리초 단위의 빈번한 프리필 중단으로 인해 50 TPS를 일관되게 초과하는 데 어려움을 겪었습니다. 반면 프리필을 분리함으로써 각 요청의 TTFT에 약 100ms의 페널티가 발생했지만, 단일 프리필러 노드가 3개의 디코더 노드에서 일관된 배치 크기를 유지할 수 있었고 디코더 노드당 약 1 QPS의 부하를 처리하면서 90 TPS를 초과하는 처리량을 제공했습니다. 데이터 병렬 배포의 경우 TPS는 약 50으로 약간 낮았지만, 인스턴스는 단일 노드당 8개 랭크로 랭크당 1 QPS의 부하를 처리할 수 있었습니다.

Qwen3-Coder

이 480B 모델은 GQA(Grouped-Query Attention)를 사용하므로 어텐션을 쉽게 샤딩할 수 있으며 KV 캐시용 메모리를 희생하지 않고도 텐서 병렬 처리의 이점을 누릴 수 있습니다. 결과적으로 프리필과 디코드 모두에 대해 8개의 GPU에 걸쳐 모델을 샤딩하고, 프리필러 노드 하나당 약 3개의 디코더 노드를 페어링할 수 있었습니다. 어텐션이 샤딩되어 있으므로 HND KV 캐시 레이아웃을 사용하여 프리필러와 디코더 KV 캐시를 샤딩하고, 프리필러 랭크와 디코더 랭크를 페어링하여 모든 NIC를 완전히 활용해 슬라이스를 병렬로 전송합니다.

인용

BibTeX
@online{perplexity2025DisaggregatedPrefillAndDecode,
  author = {{Perplexity} and Nandor Licker and Lequn Chen and Kevin Hu and Erin McKnight and Phi Hoang},
  title = {{Disaggregated prefill and decode}},
  date = {2025-08-01},
  url = {https://perplexity.coolkeypoint.com/hub/blog/disaggregated-prefill-and-decode}
}