In-House LLM Serving at Netflix

Author: Netflix AI Platform (Model Runtime & Inference 팀) | Source: netflixtechblog.com | Published: 2026-07-18


한 줄 요약

LLM 모델은 special snowflake가 되어서는 안 된다 — XGBoost 앙상블이든 대규모 LLM이든 같은 gRPC 호출로 스코어링되어 같은 클라이언트 라이브러리·헬스체크·배포 파이프라인을 재사용하고, OpenAI 호환 API는 그 위에 추가 프론트엔드로 얹는다.

핵심 주장/내용

  • 아키텍처: 멤버 규모 ML은 통합 JVM 기반 서빙 시스템이 앞단에서 라우팅·A/B 로직·후보 생성·피처 조회·추론·후처리·각 단계 로깅을 처리. 추론이 어디서 도느냐는 모델에 달렸다 — 작은 CPU 모델은 in-process로 돌아 원격 호출 오버헤드를 피하고, 큰 모델은 GPU가 필요해 전처리·후처리는 로컬에서 하되 추론을 Model Scoring Service(MSS) 에 위임. MSS는 XGBoost·TensorFlow·PyTorch·LLM을 통합 인터페이스 뒤에 지원하고 그 아래 NVIDIA Triton이 모델 로딩·배칭·GPU 스케줄링을 관리. Triton 위에 배포·버저닝·헬스체크·오토스케일링·멀티리전 롤아웃을 담당하는 Java 컨트롤 플레인
  • ① 엔진 — vLLM을 paved path로: 원래 TensorRT-LLM 기반이었지만 2025년 여름 두 가지가 바뀌었다. 오픈소스 엔진이 특화 스택과의 성능 격차를 대체로 좁혔고, 워크로드 믹스가 임베딩 생성·랭킹/검색용 prefill-only 추론·자기회귀 디코딩·비자명한 step별 제약 로직을 가진 커스텀 모델로 넓어졌다. 이 믹스로 재벤치마크하고 운영 적합성을 근거로 vLLM 선택:
    • 다단계 컴파일 파이프라인 없이 커스텀 모델 아키텍처를 로드 → 비표준 모델에서 반복이 빠름
    • 커스텀 디코딩 로직을 위한 확장 훅 → 아래의 constrained decoding 작업에 필수
    • 디버깅 용이성 — 컴파일된 엔진보다 실패와 중간 상태 검사가 쉬움
    • 친숙함 — 많은 ML 실무자가 이미 연구에서 vLLM을 쓰고 있어 research-to-production 인계 비용이 줄었다
  • ② 패키징 — 모델 아티팩트가 프론트엔드 업그레이드와 얼마나 강하게 결합되는가:
    • Python backend — 작성자가 패키징 시점에 명시적 입출력 텐서 스펙을 정의. 스펙이 아티팩트에 동결되어 벤더 프론트엔드의 request builder 기대와 일치해야 하므로, I/O 스펙을 건드리는 모든 프론트엔드 업그레이드가 패키징 코드의 조율된 변경을 요구하고 아니면 런타임에 요청이 실패
    • vLLM backend — 아티팩트가 모델 가중치와 토크나이저를 가리키는 JSON 설정일 뿐이고 Triton이 배포 시점에 I/O 텐서 스펙을 동적으로 생성. 작성자가 아예 정의하지 않으므로 모델과 프론트엔드가 독립적으로 진화한다 → 아키텍처적으로 올바른 기본값
    • 프로덕션에서 물린 두 가지: Triton/vLLM 버전 불일치(Triton의 vLLM backend가 특정 vLLM API 표면에 대해 컴파일되어, 예컨대 Triton 25.09가 vLLM 0.11.2에서 제거된 vllm.engine.metrics를 import하면 backend가 아예 로드 실패 → 서비스 이미지 빌드 시 호환 버전을 핀하고 모델 작성자의 오버라이드를 막아야 함), 커스텀 모델 로직(vLLM backend는 표준 HuggingFace 호환 모델을 기대하므로 커스텀 전/후처리, 앙상블 파이프라인, 커스텀 토크나이제이션은 execute() 전권을 주는 Python backend를 써야 함 — 이 탈출구는 일부 모델에 계속 필요할 것)
  • ③ API 표면 — 생태계 호환 HTTP 프론트엔드: 핵심 설계 목표가 LLM이 special snowflake가 아니어야 한다는 것. 그런데 OpenAI 호환 API가 LLM 생태계의 사실상 표준(추론 엔진·오케스트레이션 프레임워크·평가 도구·클라이언트 라이브러리가 모두 이 말을 한다)이 되었으므로 gRPC와 나란히 OpenAI 호환 API를 추가 프론트엔드로 노출. 보상은 실험→프로덕션 경로에서 나타난다 — 품질·레이턴시·비용·데이터 프라이버시 이유로 호스팅 모델에서 파인튜닝된 self-hosted 모델로 졸업하는 것이 거의 매끄럽다
    • 구현은 NVIDIA의 Triton OpenAI 호환 프론트엔드 재사용(임베디드 Triton 서버 시작 → TritonLLMEngine이 요청 스키마를 Triton 추론 요청으로 변환 → FastAPI로 응답). 드러난 gap 하나: response_format이 스키마에서 받아들여지지만 vLLM에 도달하기 전에 조용히 버려져 JSON 출력을 요청한 호출자가 guided decoding 제약 없이 진행되어 플랫폼이 아무 에러도 내지 않은 채 malformed JSON을 받을 수 있었다. 프론트엔드를 git-subtree해서 response_format을 vLLM의 guided decoding 파라미터로 요청 시점에 번역하도록 패치
  • ④ 배포 전략: GPU 배포는 CPU 서비스보다 기동이 오래 걸리고 모델 버전 간 I/O 스키마가 바뀔 수 있어 조율 문제가 더해진다
    • Red-Black — 신규 버전을 현행과 나란히 배포, 헬스체크 통과 후 같은 속도로 신규는 스케일업 현행은 스케일다운. 실패하면 atomic 롤백. 모델 인터페이스가 안정적일 때 옳은 선택. 프로덕션에서 드러난 조율 gap: 새 버전이 I/O 스키마 변경(예: 새 텐서 차원)을 요구하면 업스트림 소비자는 새 모델이 완전히 살아나기 전엔 설정을 업데이트할 수 없어 마이그레이션 윈도우 동안 필연적으로 “옛” 요청을 “새” 배포에 보내고 그것들이 실패한다
    • Versioned — (modelId, modelVersion) 쌍마다 독립 배포를 유지해 그 gap을 해결. 여러 버전이 동시 서빙되어 모델 배포를 소비자 업데이트와 분리한다. 대가는 전환 중첩 기간의 일시적 GPU 비용 증가
    • 권장: 텐서 shape 같은 가변 설정을 추론 모델에 직접 임베딩해 버전 무관하게 만들고 더 싼 Red-Black 경로를 쓰라. Versioned는 breaking 인터페이스 변경이 불가피한 드문 경우로 예약
  • 운영 노트: 모델 캐싱 — S3나 HuggingFace에서 시작 시 직접 다운로드하면 콜드 스타트 레이턴시가 스케줄러 허용치를 넘으므로 모델 announce 시점에 Amazon FSx에 materialize해 warm start가 오브젝트 스토리지 대신 고성능 파일시스템을 친다. 통합 metrics 엔드포인트 — vLLM은 PROMETHEUS_MULTIPROC_DIR에 .db 파일로 쓰고 Triton은 자체 Prometheus 엔드포인트로 서버 레벨 지표를 보고하는데 서로를 모르고, Triton의 내장 브리지는 40개 이상의 vLLM 지표 중 9개만 노출하며 토큰 처리량·KV 캐시 활용률·prefix 캐시 히트율 같은 핵심을 놓친다. 둘을 하나의 /metrics로 병합하는 경량 HTTP 프록시를 추가해 기존 대시보드·알림이 수정 없이 동작
  • 심층 — 대규모 constrained decoding: 추론 후에 비즈니스 로직을 적용해 무효 생성 비용을 내고 재시도·수리하는 대신 제약을 디코드 루프 안으로 밀어넣어 모델이 구조적으로 준수하는 출력을 생성하게 한다. vLLM의 custom logits processor 인터페이스로 각 제약을 생성된 토큰 이력과 함께 진화하며 매 step 토큰 자격 마스크를 방출하는 상태 기계로 모델링. 요청마다 다른 규칙을 적용하므로 요청마다 자기 processor를 갖는다
    • 첫 구현이 확장 안 된 이유 — vLLM V0에서 custom logits processor는 요청별로 실행된다. GPU가 배치 전체 logits를 생산하면 CPU가 그것을 복사해 전송을 기다리고, GIL이 Python의 요청별 작업 병렬화를 막기 때문에 제약 로직이 요청마다 순차 실행된다. 따라서 logit 처리 CPU 시간이 배치 크기에 선형으로 증가해 tail latency를 친다. 모델 forward pass는 GPU에서 효율적으로 배치되는데도 end-to-end 레이턴시가 CPU-bound가 된다. 단일 요청 벤치마크에서는 보이지 않고 현실적 동시성에서만 드러나는 병목
    • 구조적 해결은 vLLM V1 — logits 처리가 배치 레벨로 이동. processor를 배치 레벨 자료구조에서 동작하도록 재작성해 여러 요청의 마스크를 함께 계산하고, hot path를 C++ 멀티스레딩으로 재구현해 GIL을 우회. V1 API는 update_state(batch_update)로 배치 멤버십 변화를 명시적으로 추적해야 해서 V0의 요청별 인터페이스보다 복잡하지만, 동적으로 변하는 배치에서 올바른 상태를 유지하려면 필요하다. 배치 크기가 커져도 logits 처리 시간이 평평하게 유지
    • 운영 하드닝 — 설계 단계가 예상하지 못한 두 가지: partial prefill (V1의 chunked prefilling으로 요청이 여러 엔진 step에 걸쳐 prefill될 수 있는데 BatchUpdate에는 완전/부분 prefill을 구분할 granularity가 없어 내부 추적 추가), preemption (메모리 압박 시 vLLM이 부분 완료 요청의 KV 캐시를 evict하고 나중에 다른 프롬프트와 출력 토큰 리스트로 재스케줄할 수 있어 출력 토큰 리스트가 단조 증가한다는 상태 기계의 가정이 깨진다. 디코드 step 사이에 토큰 이력이 줄어드는 것을 탐지해 상태 기계를 리셋하고 새 프롬프트에서 재초기화)

주요 수치 / 사실

  • 대부분의 조직은 호스팅 API로 LLM을 소비하지만 Netflix는 별도 ML 사일로가 아닌 기존 프로덕션 환경 안에서 모델 배포부터 추론까지 풀스택을 직접 운영
  • Triton 내장 브리지가 노출하는 vLLM 지표: 40개 이상 중 9개
  • 다음 투자: system prompt 압축(품질 손실 없이 프롬프트 길이 축소), vLLM V1의 비동기 스케줄링, CPU 코드 대신 fused GPU 커널로 도는 vectorized logits processor, 메모리 footprint를 줄이고 처리량을 높이는 저정밀 모델 변형

관련 위키


Source: 원문 보기