본문으로 건너뛰기

[vllm] vLLM Rust Frontend 최적화: SSE 스트리밍 성능 개선기

PR 링크: vllm-project/vllm#51321 상태: Merged | 변경: +204 / -196

들어가며

vLLM의 Rust 기반 프론트엔드는 고성능 추론 서버를 지향하지만, 스트리밍 응답 처리 과정에서 비효율적인 메모리 사용 패턴이 발견되었습니다. 기존 구현에서는 Chat Completions 및 Completions 응답의 id, model 등 공통 필드(envelope)를 매 스트리밍 청크마다 새로 생성하고, 전체 페이로드를 임시 String으로 직렬화한 뒤 복사하는 과정을 거쳤습니다. 이는 요청이 많아질수록 CPU와 메모리 오버헤드를 가중시키는 병목이 되었습니다. 본 PR은 이 문제를 해결하기 위해 응답 envelope를 한 번만 생성하여 공유하고, 직접적인 SSE 직렬화를 도입하여 성능을 최적화했습니다.

코드 분석

1. 공통 Envelope 공유 (chat_completions.rs)

기존에는 매 청크마다 request_id, model 등을 인자로 넘겨 객체를 생성했습니다. 이를 Arc로 감싼 StreamResponseEnvelope 구조체로 추상화하여 한 번만 생성하고 재사용하도록 변경했습니다.

Before:

let mut chunk = ChatCompletionStreamResponse::new(request_id, response_model, created);

After:

let envelope = Arc::new(StreamResponseEnvelope::new(request_id, "chat.completion.chunk", created, response_model));
// ...
let mut chunk = ChatCompletionStreamResponse::new(envelope);

2. SSE 직렬화 최적화 (generate.rs, chat_completions.rs)

serde_json::to_string을 통해 임시 String을 생성하고 이를 다시 복사하던 방식을 제거했습니다. 대신 json_data 메서드를 활용하여 직접 스트림 버퍼로 직렬화하도록 개선했습니다.

Before:

let payload = serde_json::to_string(chunk).expect("...");
Event::default().data(payload)

After:

Event::default().json_data(chunk).expect("...")

왜 이게 좋은가

이번 최적화는 불필요한 메모리 할당(Allocation)과 데이터 복사(Copy)를 제거함으로써 시스템의 전체적인 처리량(Throughput)을 높이고 지연 시간(Latency)을 줄였습니다. 특히 Completions 엔드포인트에서는 처리량이 약 17% 증가하고 P99 지연 시간이 13% 이상 감소하는 유의미한 성과를 거두었습니다.

성능 측정 결과 (Completions 기준)

  • Overall output throughput: +17.4% 향상
  • P99 end-to-end latency: -13.4% 감소

교훈

  1. 반복적인 객체 생성 지양: 스트리밍처럼 빈번하게 호출되는 경로에서는 불변 객체를 공유(Arc)하는 것만으로도 큰 성능 이득을 볼 수 있습니다.
  2. 중간 버퍼 제거: 직렬화 시 임시 String을 만드는 것은 메모리 파편화와 CPU 오버헤드를 유발합니다. 가능한 한 스트림에 직접 쓰는 방식을 고려해야 합니다.
  3. 데이터 구조 설계: serdeflatten 기능을 적절히 활용하면 기존 JSON 포맷을 유지하면서도 내부 구조를 효율적으로 재설계할 수 있습니다.

참고 자료

⚠️ 알림: 이 분석은 AI가 실제 코드 diff를 기반으로 작성했습니다.

댓글

관련 포스트

PR Analysis 의 다른글