fastapi

2 개의 포스트

netflix

넷플릭스의 사내 LLM 서빙 (새 탭에서 열림)

Netflix는 LLM을 별도 ML 시스템이 아니라 기존 JVM 기반 서빙 플랫폼과 GPU 추론 백엔드 안에서 운영한다. 엔진으로는 vLLM을 선택하고, Triton·OpenAI 호환 API·통합 배포 제어 plane을 결합해 연구에서 운영까지의 전환 비용을 낮췄다. 다만 운영 환경에서는 엔진 버전 불일치, 커스텀 모델 처리, JSON 제약 누락, 모델 버전과 입력 스키마 변경 간 조정 문제가 주요 리스크로 드러났다. ## Netflix의 통합 추론 아키텍처 - 통합 JVM 서빙 시스템이 라우팅, A/B 테스트, 후보 생성, feature 조회, 추론, 후처리, 로깅을 end-to-end로 처리한다. - 실시간 요청과 캐시된 배치 경로를 모두 지원한다. - 호출 방식은 다음 두 가지다. - 기존 서빙 시스템을 통한 gRPC - 최신 LLM 애플리케이션을 위한 직접 HTTP - 소형 CPU 모델은 원격 호출 비용을 피하기 위해 서빙 프로세스 내부에서 실행한다. - 대형 GPU 모델은 Model Scoring Service(MSS)로 추론을 위임한다. - MSS는 XGBoost, TensorFlow, PyTorch, LLM을 동일한 인터페이스로 제공하며, 내부 GPU 실행은 NVIDIA Triton Inference Server가 담당한다. - Java 기반 control plane은 모델 배포, 버전 관리, 상태 점검, 자동 확장, 멀티리전 롤아웃과 무중단 업그레이드를 관리한다. ## vLLM을 표준 추론 엔진으로 선택 Netflix는 원래 Triton과 통합된 TensorRT-LLM을 사용했지만, 2025년경 워크로드 변화에 맞춰 재평가했다. - 임베딩 생성, prefill-only 추론, autoregressive decoding, 복잡한 제약 로직이 필요한 커스텀 모델까지 지원해야 했다. - vLLM을 선택한 이유: - 별도의 다단계 컴파일 없이 커스텀 모델 아키텍처 로딩 가능 - 커스텀 디코딩 로직을 위한 확장 지점 제공 - 컴파일 중심 엔진보다 장애와 중간 상태를 디버깅하기 쉬움 - 연구자들이 이미 익숙하게 사용해 연구-운영 전환 비용이 낮음 - 전문화된 엔진과 오픈소스 엔진 간 성능 격차가 줄어든 점도 선택에 영향을 주었다. ## Triton과 vLLM의 패키징 방식 Triton에서 vLLM 모델을 패키징하는 방식은 유지보수성과 프론트엔드 변경의 결합도에 큰 영향을 준다. - **Python backend** - 패키징 시 입력·출력 tensor 스펙을 직접 정의한다. - 프론트엔드의 요청 생성 방식이 바뀌면 패키징 코드도 함께 수정해야 한다. - 변경이 맞물리지 않으면 런타임 요청이 실패한다. - 전처리, 후처리, 앙상블, 커스텀 토크나이징 등 비표준 로직이 필요한 모델에는 적합하다. - **vLLM backend** - 모델 가중치와 tokenizer 경로를 담은 JSON 설정만 제공한다. - Triton이 배포 시점에 I/O tensor 스펙을 동적으로 생성한다. - 모델 아티팩트와 프론트엔드를 독립적으로 발전시킬 수 있어 기본 선택으로 적합하다. ### 운영에서 드러난 패키징 문제 - Triton의 vLLM backend는 특정 vLLM API에 맞춰 컴파일된다. - 예를 들어 Triton 25.09가 vLLM 0.11.2에서 제거된 `vllm.engine.metrics`를 import하면 backend 자체가 로드되지 않는다. - 따라서 서비스 이미지 생성 시 Triton과 vLLM 호환 버전을 고정해야 한다. - 모델 작성자가 패키징 단계에서 vLLM 버전을 임의로 덮어쓰지 못하도록 제한할 필요가 있다. - 표준 HuggingFace 모델이 아닌 커스텀 실행 흐름은 여전히 Python backend를 사용해야 한다. ## OpenAI 호환 HTTP API Netflix는 LLM을 기존 모델과 다른 “특수 모델”로 취급하지 않기 위해 모든 모델을 동일한 gRPC 호출 체계로 제공한다. - 기존 클라이언트 라이브러리, 상태 점검, 배포 파이프라인을 LLM에도 재사용한다. - 동시에 생태계 표준이 된 OpenAI 호환 API를 추가 HTTP 프론트엔드로 제공한다. - 호스팅 모델에서 자체 파인튜닝 모델로 전환할 때 API를 그대로 유지할 수 있다. - 이에 따라 품질, 지연시간, 비용, 데이터 프라이버시를 이유로 자체 호스팅을 선택해도 애플리케이션 코드 변경이 거의 없다. ## Triton 프론트엔드와 JSON 출력 제약 구현에는 NVIDIA의 Triton OpenAI 호환 프론트엔드를 활용했다. - 내장 Triton 서버가 실행된다. - `TritonLLMEngine`이 HTTP 요청 스키마를 Triton 추론 요청으로 변환한다. - FastAPI를 통해 응답을 제공한다. - KServe HTTP/gRPC 프론트엔드도 활성화해 Java control plane이 동일한 Triton 인스턴스에 gRPC로 접근할 수 있다. - 운영 중 `response_format`이 스키마에서는 허용되지만 vLLM까지 전달되지 않는 문제가 발견됐다. - 그 결과 JSON 응답을 요청해도 guided decoding이 적용되지 않아 잘못된 JSON이 반환될 수 있었다. - Netflix는 프론트엔드를 git subtree로 가져와 `response_format`을 vLLM의 guided decoding 파라미터로 변환하도록 직접 패치했다. ## 무중단 배포와 스키마 변경 문제 GPU 모델은 CPU 서비스보다 기동 시간이 길고, 모델 버전 사이에 I/O 스키마가 달라질 수 있어 배포 시 추가 조정이 필요하다. ### Red-Black 배포 - 새 버전을 기존 버전과 동시에 실행한다. - 새 인스턴스가 health check를 통과하면 트래픽을 단계적으로 이동한다. - 새 버전이 확장되는 속도만큼 기존 버전을 축소한다. - 중간 단계에서 실패하면 원자적으로 롤백한다. - 모델 인터페이스가 안정적인 경우 적합하다. - 하지만 입력 tensor 차원 추가처럼 I/O 스키마가 바뀌면 문제가 생긴다. - upstream 소비자는 새 모델이 완전히 배포되기 전까지 설정을 바꿀 수 없다. - 마이그레이션 중 새 모델에 이전 형식 요청이 전달된다. - 결과적으로 요청이 실패한다. 제공된 글은 Red-Black 방식의 한계와 `Versioned` 배포 전략을 설명하기 시작한 지점에서 끝나므로, Versioned 전략의 구체적인 동작과 장단점은 확인할 수 없다. 실무적으로는 vLLM을 표준 경로로 삼되, Triton·vLLM 버전을 강하게 고정하고 Python backend라는 예외 경로를 유지하는 것이 현실적이다. 또한 OpenAI 호환 API를 도입할 때는 스키마가 실제 추론 엔진의 제약 기능까지 전달되는지 검증하고, 모델 배포와 입력 스키마 변경을 독립적으로 조정할 수 있는 버전 관리 전략을 마련해야 한다.

kakao

음성 AI 모델을 프로덕션에 올리기까지: Kanana-O 서빙 최적화 여정 (새 탭에서 열림)

Kanana-O를 실시간 음성 대화 서비스로 제공하려면 모델 학습과는 별개의 서빙 최적화가 필요하다. 카카오는 Thinker·Talker·VoiceBox로 구성된 파이프라인에 특화된 Kanana-Omni Server를 구축해 임베딩 전달, 스트리밍 지연, 프로세스 안정성, 동시 요청 처리 문제를 해결했다. 그 결과 동시 사용자 64명 기준으로 vllm-omni 대비 상대 처리량 1.6배를 달성했다. ## Kanana-O의 멀티모달 음성 생성 구조 - **Thinker**는 텍스트·이미지·오디오 입력을 이해하고 텍스트를 생성하는 대형 언어 모델이다. - **Talker**는 Thinker의 텍스트 임베딩을 받아 음성 토큰을 순차적으로 생성한다. - **VoiceBox**는 음성 토큰을 실제 오디오 파형으로 변환한다. - 프로덕션 환경에서는 다음 조건을 동시에 만족해야 한다. - 수백 밀리초 안에 첫 음성을 제공해야 한다. - 여러 사용자의 요청을 동시에 처리해야 한다. - 텍스트와 오디오를 끊김 없이 스트리밍해야 한다. - 각 모델의 GPU 메모리 요구량 차이를 관리해야 한다. ## 범용 멀티모달 서빙 프레임워크의 한계 - Thinker가 Talker에 전달하는 값은 토큰 ID가 아니라 수천 차원의 **히든 스테이트 임베딩**이다. - 임베딩을 직렬화하거나 CPU로 복사하면 지연이 커지므로 GPU 메모리 주소를 직접 공유하는 zero-copy 방식이 필요했다. - Talker는 매 스텝 음성 토큰을 생성하지만 VoiceBox는 일정량의 토큰이 쌓인 뒤 청크 단위로 소비한다. - Talker의 입력에는 다음 값이 누적된다. - 화자 특성을 나타내는 스피커 임베딩 - Thinker의 출력 임베딩 - 이전 스텝에서 생성한 음성 임베딩 - 이처럼 생산자와 소비자의 속도가 다르고 입력 길이가 계속 증가하는 구조는 일반적인 LLM 서빙 패턴과 맞지 않았다. - 자체 서버를 구축한 결과 상대 처리량은 다음과 같았다. - naive 구현: 0.44 - vllm-omni: 1 - Kanana-Omni Server: 1.6 ## 공유 메모리와 CUDA IPC를 활용한 데이터 전달 - 프로세스 간 임베딩 전달을 매번 직렬화·역직렬화하면 토큰마다 큰 오버헤드가 발생한다. - 서버 시작 시 OS 레벨에서 공유 메모리 블록을 미리 할당해 런타임 할당·해제를 제거했다. - Thinker는 공유 메모리 풀의 블록에 데이터를 쓰고, Talker에는 실제 데이터 대신 블록 번호와 크기 같은 메타데이터만 전달한다. - 같은 노드의 GPU 간 텐서 전달에는 **CUDA IPC**를 사용했다. - GPU 텐서를 CPU로 내렸다가 다시 GPU로 올리는 `Device → Host → Device` 복사를 제거해 컴포넌트 간 통신 지연을 줄였다. ## Cascaded Streaming Pipeline - Thinker가 전체 텍스트를 생성한 뒤 Talker를 실행하는 순차 방식은 첫 음성 응답까지 수 초가 걸릴 수 있다. - 세 컴포넌트를 비동기 태스크와 큐로 연결해 파이프라인을 겹쳐 실행했다. - Thinker가 첫 청크를 생성하면 Talker는 즉시 음성 토큰 생성을 시작한다. - Talker가 앞선 토큰을 생성하는 동안 VoiceBox는 이전 토큰 청크를 오디오로 합성한다. - 각 단계가 동시에 진행되므로 전체 처리 시간보다 첫 음성까지의 체감 지연을 크게 줄일 수 있다. ## 프로세스 분리와 장애 격리 - Thinker와 Talker는 각각 독립적인 vLLM 엔진으로 실행된다. - 하나의 프로세스에서 두 엔진을 구동하면 CUDA 컨텍스트 충돌이나 GPU 메모리 관리 간섭이 발생할 수 있다. - 따라서 두 모델을 별도 프로세스로 분리하고 각자의 CUDA 컨텍스트에서 실행했다. - 프로세스 생성에는 `fork` 대신 `spawn`을 사용했다. - `fork`: 부모의 CUDA 상태와 메모리를 복제해 충돌 위험이 있다. - `spawn`: 깨끗한 Python 인터프리터에서 시작해 GPU 상태 오염을 줄인다. - Thinker가 OOM으로 종료되더라도 Talker와 API 서버까지 함께 영향을 받지 않아 독립적인 재시작과 장애 대응이 가능하다. ## vLLM continuous batching을 이용한 동시 처리 - 요청마다 이미지·오디오 입력 크기와 누적 임베딩 길이가 달라 직접 배치를 구성하기 어렵다. - 수동 배칭은 padding 낭비와 복잡한 동기화 문제를 유발한다. - Kanana-Omni Server는 배치 구성을 vLLM의 **continuous batching 스케줄러**에 위임했다. - 서버는 요청을 비동기로 빠르게 엔진에 제출하고, vLLM이 GPU 메모리와 요청 상태를 고려해 forward pass를 묶는다. - 각 요청은 `request_id`로 결과를 구분하므로 여러 요청이 하나의 배치에서 처리돼도 개별 응답을 독립적으로 스트리밍할 수 있다. - 내부 루프는 요청을 받자마자 `asyncio.create_task(generate(...))`로 실행해 다음 요청을 즉시 수락한다. ## FastAPI `workers=1`과 비동기 처리 - 일반적인 FastAPI 서버처럼 여러 워커를 실행하면 각 워커가 vLLM 모델을 별도로 로드한다. - 그 결과 GPU 메모리 사용량과 모델 로딩 시간이 워커 수만큼 증가해 멀티워커 구성이 현실적으로 어렵다. - 모델별 서버를 따로 두면 워커 확장은 가능하지만, Thinker와 Talker 사이의 임베딩이 네트워크를 거쳐 zero-copy 이점을 잃게 된다. - 따라서 API 서버는 `workers=1`로 운영하고, 단일 이벤트 루프가 블로킹되지 않도록 요청부터 오디오 생성까지 전체 경로를 `async/await`로 구성했다. - 단일 워커 환경에서는 동기 작업 하나가 모든 사용자의 처리를 막을 수 있으므로, 끝에서 끝까지 비동기 설계가 필수다. 실시간 멀티모달 모델을 서비스할 때는 모델 자체보다 모델 간 데이터 이동, 스트리밍 타이밍, GPU 메모리 관리, 장애 격리가 성능을 좌우한다. 특히 모델 간 임베딩 전달이 빈번하다면 공유 메모리와 CUDA IPC를 검토하고, 긴 생성 파이프라인은 비동기 스트리밍과 continuous batching으로 구성하는 것이 효과적이다.