원문 캡처 · vllm.ai
원문 캡처 · vllm.ai

vLLM은 대규모 언어 모델(LLM)을 위한 오픈소스 추론·서빙 엔진이다. LLM 추론은 입력 프롬프트를 처리해 키-값(KV) 캐시를 만드는 프리필 단계와, 캐시를 사용해 토큰을 하나씩 생성하는 디코드 단계로 나뉜다. 두 단계는 연산·메모리 특성과 확장 방식이 다르다. 프리필과 디코드(P-D) 분리 방식은 두 단계를 별도의 엔진 인스턴스에서 실행해 각각 독립적으로 확장하고 최적화한다. 이를 통해 부하가 걸렸을 때 자원 활용도와 처리량을 높이고 지연을 줄일 수 있다. 다만 토큰 생성을 시작하기 전에 프리필 인스턴스에서 디코드 인스턴스로 KV 캐시를 전송해야 하므로, 전송 효율이 첫 토큰까지 걸리는 시간(TTFT)에 중요하다.

vLLM은 이 전송에 NVIDIA Inference Xfer Library(NIXL) 커넥터를 사용할 수 있다. 커넥터는 AWS Elastic Fabric Adapter(AWS EFA) 같은 RDMA 지원 네트워크를 통해 KV 블록을 옮긴다. AWS EFA는 AWS 서버 간 저지연 RDMA 네트워크다. 지원 인스턴스 유형에서는 GPUDirect RDMA와 Neuron Direct RDMA를 지원해, 호스트 메모리를 거치지 않고 GPU나 Trainium 장치 메모리 사이에서 NIXL이 KV 블록을 직접 전송할 수 있다.

NIXL 커넥터의 기본 방식은 풀(pull) 방식이다. 프리필 인스턴스가 프롬프트의 KV 캐시 계산을 마치면 라우터가 프리필 블록 ID를 포함한 원본 메타데이터를 디코드 인스턴스에 전달한다. 디코드 인스턴스는 목적지 블록을 할당하고 NIXL 전송을 준비한 뒤, NIXL READ를 시작해 프리필 인스턴스에서 KV 블록을 가져온다. 이 조정과 설정 작업은 프리필이 끝난 뒤 디코딩이 시작되기 전에 순차적으로 진행되므로 첫 토큰까지 걸리는 시간의 임계 경로를 늘린다.

풀 방식 타임라인: 라우터 전달, 디코드 측 블록 할당, NIXL READ가 첫 토큰까지의 경로에서 차례로 실행된다.

이 글은 프리필 뒤의 전송 조정과 설정 작업을 임계 경로에서 옮기는 푸시 방식 NIXL 커넥터를 소개한다. vLLM으로 분리형 프리필·디코드 추론을 운영하는 고객은 이를 통해 애플리케이션 응답성을 높일 수 있다. 특히 요청률이 높을 때 TTFT와 출력 토큰당 시간(TPOT)을 줄일 수 있다.

Amazon EC2 P5en 인스턴스와 EFA를 연결한 1P1D 배포에서 Qwen3-32B를 벤치마크한 결과, 푸시 방식은 풀 방식보다 TTFT가 최대 3배, TPOT가 최대 30% 개선됐다. 자세한 내용은 성능 데이터를 참조한다. 결과는 워크로드와 구성에 따라 달라진다.

이 기능은 EFA로 연결된 AWS Trainium 인스턴스 클러스터에서 구축하고 검증한 뒤, 특정 가속기에 종속되지 않는 기능으로 vLLM에 기여했다. 여기 제시된 결과는 EFA를 사용하는 AWS p5en GPU 클러스터에서 얻었으며, GPU에서도 성능 개선이 나타났다. 이 글은 푸시 방식 아키텍처와 전송 흐름, 스레딩 모델, 성능 결과, 벤치마크 재현 방법을 설명한다.

푸시 방식 KV 전송

아키텍처 개요

푸시 방식 전송은 두 가지 이유로 TTFT와 디코드 처리량을 개선한다.

  • 생산자와 소비자의 작업을 겹쳐 실행해 풀 방식의 순차 작업을 없앤다. 생산자가 KV를 계산하는 동안 디코드 인스턴스는 목적지 블록을 미리 등록한다. 생산자 블록이 준비되면 소비자 측에서 식별자를 확인하는 왕복 과정이 끝날 때까지 기다리지 않고 전송을 시작한다. 이 왕복 과정이 첫 토큰까지의 경로에서 빠진다.
  • 전송 설정 작업을 디코드 인스턴스에서 옮긴다. NIXL 설명자를 준비하고 전송을 제출하는 작업은 프리필 측의 백그라운드 스레드에서 처리한다. 이에 따라 디코드 인스턴스는 지연에 민감한 토큰 생성 단계에서 해당 작업을 수행하지 않는다.

그 결과 TTFT가 낮아지고 디코드 인스턴스의 출력 토큰 처리량이 높아진다.

푸시 커넥터에는 세 주체가 관여한다. 디코드 인스턴스는 목적지 블록을 미리 등록하고, 프리필 인스턴스는 각 요청을 연결해 전송을 실행한다. 전용 백그라운드 스레드는 KV 쓰기 작업을 엔진의 순전파 루프 바깥에서 처리한다.

푸시 방식 KV 전송 아키텍처 개요: 디코드 인스턴스가 프리필 인스턴스에 블록을 등록하면, 프리필 인스턴스가 NIXL WRITE로 디코드 인스턴스에 KV를 보낸다.

구현 세부 사항

프로토콜은 논리적으로 세 단계로 구성된다.

  • 등록(디코드 측): 원격 프리필이 필요한 요청을 위해 디코드 인스턴스가 로컬 블록을 할당하면, 프리필 인스턴스가 KV를 쓸 위치를 설명하는 등록 정보를 만든다. 등록 정보에는 디코드 인스턴스의 식별자와 방금 예약한 블록 위치가 담긴다. 같은 엔진 단계에서 워커는 이 정보를 NIXL 알림으로 프리필 인스턴스에 보낸다. 블록 할당 시점에 등록하므로 프리필 인스턴스의 계산이 끝나기 전에 목적지 블록을 알 수 있다.
  • 랑데부(프리필 측): 디코드 측 등록 정보와 생산자 측 프리필 완료 신호는 어느 쪽이 먼저 도착해도 된다. 등록 정보가 먼저 도착하면 생산자는 프리필이 끝날 때까지 이를 보관한다. 프리필이 먼저 끝나면 등록 정보가 도착할 때까지 해당 요청의 블록을 보관한다. 나중에 도착한 쪽이 전송을 시작하게 한다. 커넥터는 요청 식별자로 두 정보를 연결하며, 재발행된 요청도 고려해 선점되거나 재시도된 요청을 올바르게 대응시킨다.
  • 전송(프리필 측): 요청이 연결되면 생산자는 아직 연결이 없는 경우 디코드 인스턴스와 연결을 만들고, NIXL WRITE를 실행해 KV 블록을 디코더가 등록한 메모리에 직접 복사한다. 전송은 각 텐서 병렬 랭크의 목적지를 지정한다. 생산자와 소비자의 내부 블록 레이아웃이 달라도 된다. 각 측은 전송 시점에 블록 위치를 자체 물리 레이아웃으로 매핑하므로 양쪽의 블록 크기가 달라도 된다. 각 WRITE에는 완료 알림이 포함되며, 데이터가 도착하면 NIXL이 디코드 인스턴스에 이를 전달한다. 디코더는 생산자 랭크마다 하나씩 도착하는 알림을 센다. 예상한 알림이 모두 도착한 뒤에야 KV 수신을 완료한 것으로 처리하고 요청을 디코딩 단계로 넘긴다.

스레딩 모델

푸시 커넥터는 각 프리필 워커에 전용 백그라운드 전송 스레드를 두어 모델 실행과 KV 전송 준비를 분리한다. 요청이 시작되면 디코드 메인 루프는 목적지 KV 블록을 할당해 프리필 인스턴스에 등록하는 한편, 프리필 메인 루프는 프롬프트의 KV 캐시를 계산한다. 목적지 등록과 프롬프트 KV가 모두 준비되면 프리필 메인 루프는 KV 블록을 백그라운드 스레드에 넘기고 모델 실행으로 빠르게 돌아간다.

백그라운드 스레드는 NIXL 연결을 만들거나 재사용하고, 요청의 KV 블록 ID를 메모리 설명자에 매핑한다. 연속된 블록을 더 큰 전송 영역으로 합친 뒤 디코드 인스턴스에 NIXL WRITE를 제출한다. NIXL이 전송을 완료하면 디코드 인스턴스에 완료 알림을 보내고, 디코드 인스턴스는 토큰 생성을 시작한다. 설명자 준비와 전송 제출을 지연에 민감한 메인 루프 바깥에서 처리하면 요청별 작업이 모델 실행을 늦추지 않는다.

푸시 경로의 단일 요청 순서도: 프리필과 디코드 인스턴스의 메인 루프 및 백그라운드 스레드 간 작업 전달을 보여준다.

풀 방식과 푸시 방식의 시간 비교

두 방식 모두 같은 NIXL 및 EFA/RDMA 데이터 경로를 사용한다. 하지만 풀 방식은 프리필 완료 뒤 메타데이터를 전달하고 디코드 측에서 전송을 준비한다. 푸시 방식은 디코드 측 설정을 프리필과 겹쳐 진행한다. 따라서 푸시 방식은 프리필 이후의 임계 경로에서 이 작업들을 제거해 디코딩을 더 일찍 시작할 수 있게 한다.

첫 토큰까지의 경로에서 풀 방식과 푸시 방식의 시간 비교.

두 방식에는 라우팅 측면의 절충점도 있다. 풀 방식은 프리필이 끝난 뒤 라우터가 디코드 인스턴스를 선택할 수 있어 부하를 고려한 라우팅과 서비스 품질 관리에 더 유연하다. 푸시 방식은 목적지 설정을 프리필과 겹치기 위해 디코드 인스턴스를 더 일찍 선택한다. 대신 부하가 비슷한 조건에서는 두 정보가 준비되는 즉시 전송을 시작하므로 프리필 인스턴스가 KV 블록을 더 빨리 해제할 수 있다.

성능 데이터

평가에는 EFA로 연결된 Amazon EC2 P5en 인스턴스의 1P1D 배포와 Qwen3-32B 모델을 사용했다. P→D 전송 성능만 분리해 측정하기 위해 --no-enable-prefix-caching 옵션으로 접두 캐싱을 비활성화했다. 이 설정에서는 디코드 노드가 매 요청마다 프리필 노드에서 전체 KV 캐시를 가져온다. 접두 캐싱을 사용하면 겹치는 프롬프트가 디코드 노드의 로컬 캐시를 재사용해 전송을 건너뛸 수 있어 비교 결과가 오해를 부를 수 있다.

푸시와 풀 KV 전송의 TTFT를 비교한 그래프는 입력 길이 1k~16k, 요청률 1~8 QPS를 측정했다. 부하가 낮고 프롬프트가 짧을 때 두 방식의 성능은 거의 같다. QPS와 입력 길이가 커지면 풀 방식의 성능이 급격히 나빠져 8 QPS, 입력 길이 16k에서 약 1,744ms에 이른다. 푸시 방식은 약 540ms로 훨씬 낮아 약 3배 줄었다. 풀 방식은 디코드 워커가 엔진 루프 안에서 동기적으로 NIXL 설명자를 제출하므로 토큰 생성이 지연된다. 푸시 방식은 이 작업을 백그라운드 스레드로 옮겨 임계 경로에서 제외한다.

EFA 연결 Amazon EC2 P5en 인스턴스의 Qwen3-32B에서 푸시와 풀 KV 전송의 TTFT를 비교한 4개 패널 그래프. 테스트 구성은 1P1D 토폴로지, 출력 토큰 128개, 디코드 노드의 접두 캐시 비활성화다.

같은 조건에서 TPOT를 측정한 그래프에서도 비슷한 양상이 나타난다. 부하가 낮을 때 푸시와 풀 방식은 약 5.6~5.8ms로 비슷하다. 부하와 입력 길이가 증가하면 풀 방식은 4~8 QPS, 입력 길이 16k에서 약 10.4ms까지 늘어난다. 푸시 방식은 약 7ms를 유지한다. 푸시 방식이 설명자 준비와 전송 제출을 디코드 워커에서 옮기기 때문이다. 토큰 생성이 작업에 밀리지 않아 동시 요청이 많고 문맥이 길어도 토큰당 지연이 안정적으로 유지된다.

EFA 연결 Amazon EC2 P5en 인스턴스의 Qwen3-32B에서 푸시와 풀 KV 전송의 TPOT를 비교한 4개 패널 그래프. 테스트 구성은 1P1D 토폴로지, 출력 토큰 128개, 디코드 노드의 접두 캐시 비활성화다.

벤치마크 실행 방법

EFA로 연결된 Amazon EC2 P5en 인스턴스에서 프리필 노드 한 대와 디코드 노드 한 대를 사용하는 1P1D 벤치마크 재현 방법은 결과 재현 안내를 참조한다. 안내에는 환경 설정, vLLM 프리필·디코드 서버 실행, 벤치마크 수행, 결과 수집 방법이 담겨 있다. 다른 지원 구성에서도 기능을 사용할 수 있지만 하드웨어, 모델, 워크로드, 네트워크 구성에 따라 성능 결과가 달라진다.

결론

푸시 방식 KV 커넥터는 프리필 계산과 디코드 측 블록 할당·등록을 겹쳐 실행한다. 프리필 이후의 라우터 왕복과 디코드 측 전송 설정을 임계 경로에서 제거해 특히 요청률이 높을 때 TTFT와 TPOT를 줄인다. EFA를 사용하는 Amazon EC2 P5en 인스턴스의 1P1D Qwen3-32B 벤치마크에서 푸시 방식은 풀 방식보다 TTFT를 최대 3배, TPOT를 30% 개선했다. 결과는 워크로드와 구성에 따라 달라진다. 푸시 방식을 사용하려면 프리필 서버와 디코드 서버 모두에서 NixlConnector 대신 NixlPushConnector를 설정한다.

디코드 노드를 일찍 선택해도 되고 TTFT를 낮추는 것이 우선인 경우 푸시 커넥터를 선택할 수 있다. 양방향 KV 전송이 필요하거나 부하 기반 라우팅을 위해 프리필 이후에 디코드 인스턴스를 선택해야 하는 워크로드에는 풀 커넥터가 더 적합하다. vLLM은 푸시와 풀 KV 커넥터를 모두 지원하므로 배포 환경에 맞춰 선택할 수 있다.

향후 개선으로는 백그라운드 NIXL 스레딩 모델을 풀 방식 커넥터에도 적용해 NIXL 설정을 임계 경로에서 옮기는 방안을 검토하고 있다. 이를 통해 풀 방식 워크로드도 같은 최적화를 활용할 수 있다. 이를 살펴보기 위한 풀 리퀘스트(#45211)를 열었다.

참고 자료: vLLM 분리형 서빙 안내, AWS EFA, 푸시 방식 NIXL KV 커넥터.

vLLM 커뮤니티가 이 기능을 유지하는 데 지원을 제공한 데 감사를 전한다.

부록: 결과 재현

토폴로지는 EFA를 사용하는 p5en GPU 노드 두 대로 구성한다. 프리필 노드는 :8100, 디코드 노드는 :8200에서 실행한다. 프록시와 벤치마크 클라이언트는 별도의 소형 노드에서 실행한다. 이 글에는 스크립트 세 개가 제공된다. serve_pd.sh는 두 방식 중 하나로 프리필 또는 디코드 서버를 실행한다. run_sweep.sh는 스윕을 실행한다. cleanup.sh는 실행 환경을 정리한다. plot_push_vs_pull.py는 결과 JSON 파일에서 위 두 그래프를 다시 그린다.

테스트 환경

두 GPU 노드는 다음과 같이 동일하게 설정했다.

  • 인스턴스 유형: p5en.48xlarge(NVIDIA H200 8개). 프리필과 디코드에 각각 한 대를 사용했다.
  • 리전 및 가용 영역: ap-south-1(뭄바이), ap-south-1b(AZ ID aps1-az3). 두 노드는 같은 AZ에 있었다.
  • 배치: 클러스터 배치 그룹을 사용하지 않았다. 두 인스턴스는 대상 지정 온디맨드 용량 예약에 시작했으며 기본 테넌시를 사용했다.
  • AMI: Ubuntu 24.04 x86_64용 AWS ParallelCluster 3.14.2 퍼블릭 이미지 aws-parallelcluster-3.14.2-ubuntu-2404-lts-hvm-x86_64-202602121713를 사용했다. ap-south-1의 AMI ID는 ami-0133f195033a4baae이며, AMI ID는 리전에 따라 다르다. 기본 이미지에는 커널 6.14.0-1018-aws, NVIDIA 드라이버 570, CUDA 12.8이 포함되어 있어 아래 버전으로 드라이버, CUDA 툴킷, EFA 설치 프로그램을 업그레이드했다.
  • 운영체제·커널: Ubuntu 24.04.4 LTS, 6.14.0-1018-aws.
  • NVIDIA 드라이버·CUDA: 드라이버 580.126.16(CUDA 13.0 드라이버 API), CUDA 툴킷 13.0.
  • EFA 인터페이스: 인스턴스당 16개로 네트워크 카드마다 하나씩 있다. 카드 0~15는 모두 인터페이스 유형 efa이며, 각 인터페이스는 200 Gb/s로 총 3.2 Tb/s를 제공한다. NUMA 노드마다 인터페이스 8개가 있고, GPU마다 PCIe 스위치를 공유하는 인터페이스가 2개 있다.
  • EFA 소프트웨어: EFA 설치 프로그램 1.50.0, EFA 커널 모듈 3.3.0g, libfabric 2.6.0amzn1.0, rdma-core 64.0amzn0.

NIXL의 EFA 장치 선택 방식은 다음과 같다. LIBFABRIC 백엔드는 efa 공급자를 사용해 EFA 장치 16개를 모두 레일로 연다. 그런 다음 각 GPU의 KV 캐시를 해당 GPU와 PCIe 스위치를 공유하는 레일 2개에만 등록해 모든 전송이 PCIe 내부에서 이뤄지도록 한다. NIXL이나 libfabric의 장치 재정의 설정은 하지 않았으며 기본 선택을 사용했다. 자체 노드에서 이를 확인하려면 서버를 NIXL_LOG_LEVEL=DEBUG로 실행하고 등록 로그를 확인한다.

grep -E "Created rail [0-9]+ \\(device=|mapped to rail" server.log

노드 준비

두 GPU 노드에 EFA 사용자 공간 스택을 설치하고 공급자가 있는지 확인한다. 사용한 설치 프로그램은 1.50.0이다.

curl -O https://efa-installer.amazonaws.com/aws-efa-installer-1.50.0.tar.gz tar -xf aws-efa-installer-1.50.0.tar.gz && cd aws-efa-installer sudo ./efa_installer.sh -y fi_info --version # libfabric 2.6.0amzn1.0 fi_info -p efa # verify the EFA provider is present

두 GPU 노드는 서로 8100·8200 포트와 NIXL 사이드 채널(5600)을 통해 연결될 수 있어야 한다.

vLLM과 NIXL 설치

두 GPU 노드에서 실행한다.

python3.12 -m venv ~/vllm_env && source ~/vllm_env/bin/activate pip install --upgrade pip pip install vllm==0.29.0 # any build that includes the NIXL push connector pip install nixl==1.4.1 # NIXL with the libfabric/EFA backend

GPU 런타임이 필요하지 않은 프록시 노드에서는 다음을 실행한다.

pip install vllm aiohttp fastapi uvicorn pandas datasets

모델 다운로드

export HF_HOME=/shared/huggingface # a large shared volume hf download Qwen/Qwen3-32B

프리필·디코드 쌍 실행

각 GPU 노드에서 serve_pd.sh를 한 번씩 실행한다. 두 모드의 차이는 kv_connector뿐이다. 풀 방식에는 NixlConnector, 푸시 방식에는 NixlPushConnector를 사용한다. 포트, 엔진 ID, 역할, 랭크는 그대로 둔다.

# on the prefill node PREFILL_IP=<PREFILL_IP> DECODE_IP=<DECODE_IP> ROLE=prefill MODE=pull ./serve_pd.sh # on the decode node PREFILL_IP=<PREFILL_IP> DECODE_IP=<DECODE_IP> ROLE=decode MODE=pull ./serve_pd.sh

프리필 측에서는 다음 명령으로 확장된다.

vllm serve Qwen/Qwen3-32B --served-model-name Qwen --dtype bfloat16 \\ --tensor-parallel-size 8 --max-model-len 32768 --max-num-seqs 8 --port 8100 \\ --kv-transfer-config '{"kv_connector":"NixlConnector","kv_buffer_device":"cuda", "kv_role":"kv_producer","engine_id":"prefill-engine-001","kv_rank":0, "kv_parallel_size":2,"kv_connector_extra_config":{"backends":["LIBFABRIC"]}}'

디코드 서버에는 --no-enable-prefix-caching을 추가한다. 이 설정은 측정 대상인 프리필에서의 전체 KV 가져오기가 매 요청마다 이뤄지게 한다. 캐시를 켜면 겹치는 프롬프트가 디코드 노드의 로컬 캐시를 적중시켜 P→D 전송을 건너뛸 수 있고, 결과가 오해를 부를 수 있다. 벤치마크를 실행하기 전에 두 서버의 /health 엔드포인트가 모두 200을 반환하는지 확인한다.

스윕 실행

벤치마크 스크립트는 #57567에 있다. 모드에 맞는 프록시(disagg_proxy_demo.py는 풀, disagg_proxy_pushconnector_demo.py는 푸시)를 선택해 vllm bench serve로 실행한다.

git clone https://github.com/vllm-project/vllm.git ~/vllm && cd ~/vllm git fetch origin pull/57567/head:pd-bench && git checkout pd-bench export VLLM_SRC=~/vllm VLLM_SRC=~/vllm PREFILL_IP=<PREFILL_IP> DECODE_IP=<DECODE_IP> \\ MODE=pull ./run_sweep.sh

두 서버를 중지하고 4단계의 명령을 각 노드에서 MODE=push로 다시 실행한 뒤 푸시 모드 스윕을 수행한다. 푸시 방식에서는 프록시가 디코드 노드에 등록 위치를 알려줄 수 있도록 프리필 인스턴스의 NIXL 좌표도 필요하다. 변수는 PREFILL_ENGINE_ID, PREFILL_KV_HOST, PREFILL_SIDE_CHANNEL_PORT, PREFILL_TP_SIZE, PREFILL_PP_SIZE다. run_sweep.sh는 위 기본값을 사용해 이 값을 채운다. 두 실행에서 QPS_LIST, INPUT_LENS, OUTPUT_LENS를 동일하게 유지하고, PREFILL_TP_SIZE가 프리필 서버의 실제 텐서 병렬 크기와 일치하는지 확인한다.

results/에는 <mode>_qps<q>_in<in>_out<out>_iter<i>.json 파일과 .log 파일이 저장된다. JSON에는 실행별 TTFT, ITL, 처리량, 종단 간 지연이 들어 있다. 그래프를 다시 그릴 때는 다음 명령을 사용한다.

python plot_push_vs_pull.py --results-dir ./results --metric ttft python plot_push_vs_pull.py --results-dir ./results --metric tpot

문제 해결

  • 첫 요청 직후 서버가 중단되면 모드와 커넥터 설정이 맞는지 확인한다. 풀 방식은 P와 D 모두 NixlConnector, 푸시 방식은 P와 D 모두 NixlPushConnector를 사용해야 한다.
  • Backend 404: model ... does not exist 오류가 나면 SERVED_MODEL_NAME이 서버의 --served-model-name 값(Qwen)과 일치하는지 확인한다.
  • 프록시 준비 상태 확인이 시간 초과되면 선택한 모드의 프록시가 $VLLM_SRC/examples/disaggregated/disaggregated_serving/ 아래에 있는지, P와 D의 /health가 모두 200을 반환하는지 확인한다.
  • NIXL 핸드셰이크나 전송 오류가 발생하면 EFA(fi_info -p efa)와 P·D 사이드 채널 포트(5600) 개방 여부를 확인한다. PREFILL_TP_SIZE가 프리필 서버의 텐서 병렬 크기와 일치하는지도 확인한다.

정리

./cleanup.sh proxy # on the proxy / benchmark driver node ./cleanup.sh server # on each GPU node

그런 다음 AWS EC2 사용자 안내에 따라 인스턴스를 종료한다.