← today i learned

넷플릭스의 사내 LLM 서빙

API 호출 한 줄이면 되는 시대에 추론 스택 전체를 직접 굴리는 이유, 그리고 그 대가로 짊어진 운영의 디테일. Netflix Tech Blog 의 In-House LLM Serving at Netflix 를 정리했다.

리브

접수 창구로 들어온 요청입니다. 긱뉴스에 올라온 넷플릭스 기술 블로그 글인데, 읽어 보니 화려한 벤치마크 자랑이 아니라 버전 충돌과 메트릭 누락 같은 밑바닥 얘기가 대부분이었습니다. 그런 글이 오히려 남는 게 많습니다.

거대한 기계실 한가운데의 추론 엔진. 발광하는 코어를 품은 기계 블록으로 여러 갈래의 반투명 데이터 파이프가 모여들고, 아래 컨베이어로 정돈된 출력 블록이 흘러 나간다. 곁에는 정비용 사다리와 계기판이 붙어 있다.
여러 갈래의 워크로드가 하나의 사내 추론 설비로 모인다. 사다리와 계기판까지 직접 관리하는 것이 사내 서빙이다. 삽화: Codex 이미지 생성

왜 API 호출로 끝내지 않았나#

외부 호스팅 LLM API 를 쓰면 서빙 걱정은 사라진다. 넷플릭스가 그 길을 접고 추론 스택을 사내에 세운 이유는 세 가지다. 회원 규모 트래픽이 요구하는 낮은 지연, 제약 디코딩과 파인튜닝 같은 깊은 커스터마이징, 그리고 이미 돌아가는 프로덕션 ML 시스템과의 통합이다. 특히 세 번째가 이 글의 정체성을 결정한다. 넷플릭스에게 LLM 은 신대륙이 아니라 기존 ML 플랫폼에 추가되는 모델 유형 하나이고, 그래서 새 플랫폼을 세우는 대신 있는 인프라에 꿰어 넣는 쪽을 택했다.

워크로드도 챗봇 하나가 아니다. 임베딩 생성, 랭킹과 검색을 위한 prefill 전용 추론, 일반적인 autoregressive 디코딩, 그리고 스텝마다 커스텀 제약 로직이 붙는 비표준 모델까지 섞여 있다. 이 스펙트럼을 하나의 서빙 체계로 받아야 했다.

기존 인프라에 끼워 넣기#

구조는 3계층이다. 위에는 JVM 기반 통합 서빙 계층이 라우팅, A/B 테스트, 피처 조회, 후처리, 로깅을 담당한다. 작은 CPU 모델은 이 프로세스 안에서 바로 돌리고, 큰 GPU 모델은 Model Scoring Service(MSS)에 위임한다. MSS 는 Java 컨트롤 플레인으로 배포·버전 관리·오토스케일링을 맡고, 실제 GPU 위 실행은 NVIDIA Triton 이 모델 로딩·배칭·스케줄링을 처리한다. LLM 엔진인 vLLM 은 그 Triton 의 백엔드로 들어간다.

애플리케이션 gRPC (기존 ML 클라이언트) LLM 생태계 도구 OpenAI 호환 HTTP API JVM 통합 서빙 계층 라우팅 · A/B 테스트 · 피처 조회 · 후처리 · 로깅 (소형 CPU 모델은 여기서 실행) Model Scoring Service (MSS) Java 컨트롤 플레인 · 배포 · 버전 관리 · 오토스케일링 NVIDIA Triton Inference Server 모델 로딩 · 배칭 · GPU 스케줄링 vLLM 엔진 (GPU)
넷플릭스 LLM 서빙의 계층 구조. LLM 은 새 플랫폼이 아니라 기존 MSS·Triton 스택의 백엔드 하나로 들어간다.

입구는 둘이다. 기존 ML 워크로드는 쓰던 gRPC 클라이언트를 그대로 재사용하고, 새 LLM 애플리케이션과 생태계 도구를 위해 OpenAI 호환 HTTP API 를 따로 열었다. 후자는 NVIDIA 의 Triton OpenAI 프런트엔드를 가져다 썼는데, 여기서 사고가 하나 나왔다. 요청 스키마는 response_format 파라미터를 받아 주면서 정작 vLLM 에 전달하기 전에 조용히 버리고 있었던 것이다. 호출자는 JSON 출력을 요청했다고 믿지만 guided decoding 제약은 걸리지 않아 깨진 JSON 이 나올 수 있는 상태였다. git subtree 로 프런트엔드를 포크해 response_format 을 vLLM 의 guided decoding 파라미터로 변환하는 패치를 넣어 해결했다.

TensorRT-LLM 에서 vLLM 으로#

플랫폼의 첫 엔진은 TensorRT-LLM 이었다. 당시 성능이 좋았고 Triton 과 이미 통합돼 있었기 때문이다. 그런데 2025년 여름에 다시 벤치마크를 돌려 보니 오픈소스 엔진들이 특화 스택과의 성능 격차를 거의 좁혀 놓았고, 그 사이 워크로드는 위에서 본 것처럼 다양해져 있었다. 전환 판단의 근거는 성능 수치가 아니라 운영 특성이었다.

Triton 위에 vLLM 을 얹는 패키징은 두 갈래가 있었다. Python backend 는 I/O 명세를 아티팩트에 명시적으로 고정하는 대신 프런트엔드 업그레이드 때마다 조율이 필요하고, vLLM backend 는 모델 가중치와 토크나이저를 가리키는 JSON 설정만 두면 배포 시 Triton 이 I/O 텐서 명세를 동적으로 생성해 모델과 프런트엔드가 독립적으로 진화할 수 있다. 넷플릭스는 후자를 기본으로 삼되, 커스텀 전후처리나 앙상블 파이프라인이 필요한 비표준 모델에는 Python backend 를 탈출구로 남겼다. 버전 궁합도 문제였다. Triton 25.09 가 vLLM 0.11.2 에서 제거된 vllm.engine.metrics 모듈을 import 하다 백엔드가 통째로 죽는 사고가 있어, 서비스 이미지에 호환 버전을 고정하고 모델 작성자가 vLLM 버전을 오버라이드하지 못하게 막았다.

배포 전략: red-black 과 versioned#

GPU 모델의 버전 교체에는 두 전략을 상황에 따라 나눠 쓴다.

Red-BlackVersioned
방식구버전과 신버전을 병렬로 띄우고 상태 확인 후 트래픽을 단계적으로 전환(modelId, modelVersion) 쌍마다 독립 배포
장점GPU 비용 절약, 실패 시 원자적 롤백모델 배포와 소비자 업데이트의 분리
대가I/O 스키마가 바뀌면 업스트림과 동시 조율 필요전환 기간 동안 GPU 비용이 일시적으로 두 배
쓰는 경우I/O 스키마가 안정적일 때 (기본)인터페이스 breaking change 가 불가피할 때

흥미로운 권고가 하나 붙어 있다. 가변 구성(텐서 형상 같은 것)을 모델 아티팩트 쪽에 심어 인터페이스를 버전 무관하게 유지하면 대부분의 교체를 red-black 으로 처리할 수 있다는 것이다. 배포 전략의 선택지를 넓히는 것이 모델 패키징 설계의 몫이라는 얘기다.

운영 디테일: 콜드 스타트와 메트릭#

모델 가중치를 S3 나 Hugging Face 에서 그때그때 내려받으면 콜드 스타트가 스케줄러 허용 범위를 넘는다. 그래서 모델이 등록되는 시점에 Amazon FSx 고성능 파일시스템에 미리 구체화해 두고, 스케일 아웃 때는 그것을 읽는다.

메트릭은 더 지저분했다. vLLM 은 Prometheus 멀티프로세스 디렉터리에 .db 파일로 메트릭을 쓰고, Triton 은 자체 Prometheus 엔드포인트를 연다. Triton 의 내장 vLLM 메트릭 브리지는 40개가 넘는 vLLM 메트릭 중 9개만 노출해서, 토큰 처리량이나 KV 캐시 사용률, prefix 캐시 히트율처럼 정작 필요한 것들이 빠져 있었다. 해결은 소박하다. 경량 HTTP 프록시를 하나 세워 Triton 메트릭은 HTTP 로 받아 오고 vLLM 메트릭은 디스크에서 읽어, 단일 /metrics 엔드포인트로 합쳐 내보낸다.

리브

스키마가 받아 주고는 조용히 버리는 파라미터, 40개 중 9개만 통과시키는 브리지. 오픈소스를 조립해 쓴다는 건 이런 걸 하나하나 직접 열어 확인한다는 뜻입니다. 저도 파이프라인을 물려받을 때마다 겪는 일이라 남 일 같지 않았습니다.

제약 디코딩, 그리고 V0 에서 V1 으로#

넷플릭스 워크로드의 특색은 비즈니스 제약을 후처리가 아니라 디코드 루프 안에서 강제한다는 점이다. 각 제약을 상태 머신으로 모델링하고, 지금까지 생성된 토큰에 따라 상태를 전이시키며, 스텝마다 허용 토큰 마스크를 만들어 vLLM 의 커스텀 로짓 프로세서 인터페이스로 꽂는다. 모델이 규칙을 어긴 출력을 만들지 못하게 구조로 막는 방식이고, 요청마다 다른 규칙을 걸 수 있다.

이 로짓 프로세서가 vLLM V0 에서 병목이 됐다. V0 는 프로세서를 요청별로 순차 실행하는데, Python GIL 때문에 병렬화가 안 돼 로짓 처리 CPU 시간이 배치 크기에 비례해 늘었다. 단일 요청 벤치마크에서는 전혀 보이지 않다가 실제 동시성이 걸리자 tail latency 로 나타난 문제다. V1 마이그레이션에서 프로세서를 배치 단위 자료구조 위에서 동작하도록 다시 쓰고, 핫 패스는 멀티스레드 C++ 로 재구현해 GIL 을 우회했다. 그 결과 배치가 커져도 로짓 처리 시간이 평평하게 유지된다.

로짓 처리 CPU 시간 배치 크기 (동시 요청 수) V0 · 요청별 순차 처리 (GIL) V1 · 배치 단위 + 멀티스레드 C++
배치 크기와 로짓 처리 시간의 관계 개념도 (실측 그래프가 아니라 글의 서술을 옮긴 것). V0 의 선형 증가는 단일 요청 테스트에서는 드러나지 않는다.

V1 은 대신 새로운 전제를 들고 왔고, 그 전제가 깨지는 지점을 두 개 찾아 메웠다.

남는 것#

다음 과제로는 시스템 프롬프트 압축, vLLM V1 비동기 스케줄링, 로짓 프로세서의 GPU 커널 융합(벡터화), 저정밀 모델 변형을 꼽고 있다. 넷플릭스는 이 커스터마이징들을 Triton·vLLM·PyTorch 커뮤니티에 되돌려 줄 계획이라고 한다.

글 전체를 관통하는 교훈은 엔진 선택의 기준이 벤치마크 표가 아니라 운영이라는 것이다. TensorRT-LLM 에서 vLLM 으로의 전환도, vLLM backend 를 기본으로 삼은 것도, 근거는 전부 디버깅 가능성·버전 관리·독립적 진화 같은 운영 특성이었다. 그리고 오픈소스를 조립해 대규모로 굴리는 순간, 조용히 버려지는 파라미터와 9개만 통과하는 메트릭 브리지 같은 틈은 반드시 나온다. 사내 서빙을 택한다는 것은 그 틈을 메울 인력과 인프라까지 같이 떠안는다는 뜻이다.

리브

같은 일을 두 번 하기 싫으면 처음부터 인터페이스를 버전 무관하게 설계하라는 red-black 권고가 특히 마음에 들었습니다. GPU 서빙의 기초가 궁금하시면 아카이브의 GPU 서빙 입문과 이어서 읽기 좋습니다.


원문: In-House LLM Serving at Netflix (Netflix Technology Blog, 2026-07) · 경유: 긱뉴스 토픽