넷플릭스는 왜 LLM을 직접 서빙하는가 (2부): vLLM과 Triton, 서빙 엔진을 해부하다
1부에서 '왜'를 봤다면 2부는 '어떻게'다. 넷플릭스 서빙 스택의 심장인 vLLM과 Triton을 뜯어본다. KV 캐시가 왜 메모리를 잡아먹는지, PagedAttention이 어떻게 그걸 운영체제의 페이징으로 해결했는지, 연속 배칭이 왜 처리량을 몇 배로 끌어올리는지 — 생소한 용어를 하나씩 그림으로 풀고, 넷플릭스가 TensorRT-LLM에서 vLLM으로 갈아탄 진짜 이유까지 따라간다.
코어닷투데이2026-07-3033분
다시, 그 '문' 안으로
1부에서 우리는 넷플릭스가 왜 LLM을 직접 서빙하기로 했는지를 봤다. 그리고 "모든 모델은 같은 문으로 통과한다"는 철학에서 멈췄다. 2부는 그 문 안으로 들어가 엔진을 직접 뜯어본다.
경고: 여기서부터 낯선 이름들이 쏟아진다. vLLM, Triton, KV 캐시, PagedAttention, 연속 배칭… 하지만 걱정 말자. 하나하나가 사실은 아주 상식적인 아이디어이고, 대부분은 우리가 이미 아는 것(도서관, 운영체제, 식당 주방)에 비유할 수 있다. 순서대로 따라오면 된다.
넷플릭스 원문에는 전체 구조를 담은 그림(Figure 1)이 있다. 그걸 우리 방식으로 다시 그리면 이렇다. 요청이 어디서 들어와 어디로 흘러가는지를 보여주는 지도다.
소비자 앱 A 예: 페이지 구성 서비스 (홈 화면을 그림)
↓ "모델 서빙해줘" gRPC 호출
통합 서빙 시스템 (JVM 기반) 라우팅 · A/B 테스트 · 후보 생성 · 피처 조회 · 추론 위임 · 후처리 · 로깅
↓ "스코어 매겨줘(scoreModel)" gRPC 호출
MSS · Model Scoring Service XGBoost · TensorFlow · PyTorch · LLM을 하나의 인터페이스로 ───────── 내부: NVIDIA Triton(모델 로딩·배칭·GPU 스케줄링) + vLLM(추론)
↑ REST HTTP 직접 호출
소비자 앱 B 예: 대화형 추천 서비스 (새로운 LLM 기반 앱)
읽는 법은 이렇다.
작은 CPU 모델은 그 자리에서(in-process) 처리된다. 굳이 다른 서버에 물어보는 왕복 비용을 아끼려는 것이다.
큰 모델(LLM)은 GPU가 필요하다. 그래서 통합 서빙 시스템은 전처리·후처리만 자기가 하고, 정작 무거운 추론은 MSS(Model Scoring Service) 라는 별도의 공용 백엔드에 위임한다.
MSS 안에서 역할이 갈린다. Triton은 모델을 관리·스케줄링하는 '관리자', vLLM은 실제로 추론을 돌리는 '엔진'이다.
두 개의 진입로에 주목하자. 기존 ML 앱은 왼쪽 위처럼 gRPC로 들어오고, 새로운 LLM 앱은 오른쪽 아래처럼 REST HTTP로 직접 들어온다. 왜 두 갈래인지는 이 글 뒤에서 밝혀진다.
이제 이 지도의 핵심 부품인 vLLM부터 파헤치자.
vLLM: KV 캐시라는 골칫거리에서 태어나다
vLLM을 이해하려면 먼저 LLM이 답을 만드는 방식을 알아야 한다. 겁먹지 말자. 핵심은 딱 하나다.
LLM은 답을 한 번에 쓰지 않는다. 토큰(단어 조각)을 하나씩, 앞의 것을 보고 다음 것을 붙이며 이어 쓴다.
"나는 오늘 점심에 ___" 다음에 "김밥"을 붙이고, 그다음 "을"을 붙이고… 이렇게 한 글자씩 이어가는 것을 자기회귀(autoregressive) 생성이라 한다. 문제는 여기서 시작된다. 다음 토큰을 고를 때마다 앞에 나온 모든 토큰을 다시 참고해야 한다. 매번 처음부터 다시 계산하면 너무 느리다. 그래서 앞 토큰들의 계산 결과를 저장해두는데, 이 저장고가 바로 KV 캐시(Key-Value Cache) 다.
옛날 방식의 낭비
KV 캐시는 문장이 길어질수록 커진다. 문제는 문장이 얼마나 길어질지 미리 모른다는 것. 그래서 vLLM 이전의 시스템들은 안전하게 "최대 길이만큼 방을 통째로 예약" 하는 방식을 썼다. 마치 3명이 올지 30명이 올지 모르니 일단 30인용 연회장을 통째로 잡아두는 식이다.
결과는 처참했다. 연구에 따르면 예전 시스템은 KV 캐시 메모리의 20~40%만 실제로 쓰고 나머지는 낭비했다. GPU 메모리는 세상에서 가장 비싼 부동산인데, 그 절반 이상을 빈 방으로 놀린 것이다.
예전 방식 (통째 예약)
실사용 ~30%
PagedAttention
실사용 ~96%
PagedAttention: 운영체제에서 훔쳐온 해법
2023년, UC 버클리의 스카이 컴퓨팅 랩이 이 문제를 정면으로 겨눈 논문을 냈다. 「Efficient Memory Management for LLM Serving with PagedAttention」(SOSP 2023). 이 논문에서 태어난 오픈소스가 바로 vLLM이다.
아이디어의 천재성은, 완전히 새로운 걸 발명하지 않았다는 데 있다. 운영체제(OS)가 수십 년 전에 똑같은 문제를 이미 풀어놨다는 걸 알아챈 것이다. OS는 프로그램에게 메모리를 통째로 주지 않는다. 작은 '페이지' 단위로 잘라서, 물리 메모리 여기저기 흩어진 빈 칸에 넣고, '페이지 테이블'이라는 지도로 어디에 뭐가 있는지 추적한다.
효과는 극적이었다. 미리 예약하는 낭비가 사라지니(마지막 블록만 살짝 빌 뿐), 같은 GPU에 훨씬 많은 요청을 동시에 태울 수 있게 됐다. 논문은 기존 시스템 대비 처리량 2~4배를 보고했다. 게다가 블록을 여러 문장이 공유할 수도 있다 — 똑같은 시스템 프롬프트를 100명이 쓴다면, 그 부분의 블록은 한 벌만 두고 100명이 가리키게 하면 된다.
한 문장 정리: PagedAttention은 "GPU 메모리를 운영체제가 RAM 관리하듯 페이지 단위로 관리하자"는 아이디어다. 이 하나로 vLLM은 LLM 서빙의 사실상 표준이 됐다.
연속 배칭: 식당 주방의 지혜
메모리를 아꼈으니, 이제 여러 요청을 어떻게 동시에 처리하느냐가 남았다. 여기서 두 번째 핵심 개념, 연속 배칭(continuous batching) 이 등장한다.
GPU는 요청을 하나씩 처리하면 엄청나게 비효율적이다. 여러 개를 묶어서(batch) 한 번에 처리해야 제값을 한다. 문제는 묶는 방식이다.
방식
정적 배칭 (옛날)
연속 배칭 (vLLM)
묶는 시점
시작할 때 8명 묶으면 끝
매 스텝마다 다시 편성
빨리 끝난 요청
가장 느린 요청 끝날 때까지 대기
즉시 나가고 빈자리 채움
새 요청
배치 다 끝날 때까지 대기
바로 합류
GPU 활용도
낮음 (놀림)
항상 가득
식당으로 비유하면 이렇다. 정적 배칭은 8인 테이블을 다 채워야 주문을 받고, 그 8명이 전부 식사를 끝내야 다음 손님을 받는다. 3명이 먼저 다 먹어도 나머지 5명 눈치를 보며 앉아 있어야 한다. 반면 연속 배칭은 다 먹은 사람은 바로 일어나 보내고, 그 자리에 대기 중인 새 손님을 즉시 앉힌다. 주방(GPU)은 단 한 순간도 놀지 않는다.
이 아이디어의 원조는 2022년 서울대·프렌들리AI 팀의 「Orca」 논문(OSDI 2022)이다. "요청 단위가 아니라 반복(iteration) 한 스텝 단위로 스케줄링하자"는 발상으로, 기존 대비 처리량을 최대 수십 배까지 끌어올렸다. vLLM은 이 연속 배칭 + PagedAttention을 결합했다. 페이징이 유연한 메모리 공간을 공급하고, 연속 배칭이 그 공간을 한순간도 비우지 않고 채운다. 이 둘의 조합이 vLLM의 심장이다.
두 얼굴의 계산: 프리필과 디코드
한 걸음 더 들어가자. LLM 추론은 성격이 전혀 다른 두 단계로 나뉜다. 이 구분은 3부의 성능 이야기에서 결정적이므로 지금 확실히 잡아두자.
① 프리필 (Prefill) 프롬프트 전체를 한 번에 읽어 KV 캐시를 채우는 단계. 계산량이 많음 = 연산 병목(compute-bound). "질문을 통째로 이해하는" 단계.
② 디코드 (Decode) 답 토큰을 하나씩 생성하는 단계. 매 스텝 모델 가중치를 다시 읽음 = 메모리 대역폭 병목. "답을 한 글자씩 쓰는" 단계.
여기서 재미있는 파생 개념 둘.
프리필만 하는(prefill-only) 추론: 임베딩·랭킹·분류처럼 "답을 생성"할 필요 없이 입력을 한 번 읽고 점수만 뽑으면 되는 작업이 있다. 이건 디코드 없이 프리필만 하는 셈이다. 넷플릭스의 추천/랭킹 워크로드가 여기 해당한다. 넷플릭스가 vLLM을 고른 이유 중 하나가 바로 이런 다양한 워크로드(임베딩·프리필 전용·자기회귀 생성)를 한 엔진으로 처리할 수 있어서다.
청크 프리필(chunked prefill): 아주 긴 프롬프트의 프리필은 시간을 오래 잡아먹어 다른 요청을 멈추게 한다. 그래서 긴 프리필을 여러 조각으로 잘라 진행 중인 디코드 작업과 번갈아 끼워 넣는다. 연산 병목인 프리필과 메모리 병목인 디코드를 섞으면 GPU를 더 알뜰하게 쓴다. (이 청크 프리필이 3부에서 골칫거리를 하나 만든다. 기억해두자.)
넷플릭스는 왜 vLLM으로 갈아탔나
이제 넷플릭스의 결정적 선택으로 돌아온다. 놀랍게도 넷플릭스의 플랫폼은 처음엔 vLLM이 아니었다. 초기엔 NVIDIA의 TensorRT-LLM으로 지어졌다. 당시 가장 빠른 엔진이었고 Triton과 이미 통합돼 있었기 때문이다.
TensorRT-LLM은 강력하다. 모델을 미리 컴파일해 GPU에 최적화된 엔진으로 구워낸다. 커널 융합, 양자화 같은 기법으로 최고 성능을 낸다. 하지만 대가가 있다.
!
TensorRT-LLM의 값
쓰기 전에 컴파일 파이프라인을 거쳐야 한다. 최대 길이·배치 크기 등을 미리 못 박아 엔진을 구워야 하고, 잘못 정하면 워크로드를 못 태운다. 커스텀 모델을 얹기 어렵고, 문제가 나도 내부를 들여다보기(디버깅) 어렵다.
→
2025년 여름, 두 가지가 바뀌었다
첫째, 오픈소스 엔진이 성능 격차를 거의 다 따라잡았다. 둘째, 넷플릭스의 워크로드가 넓어졌다(임베딩·프리필 전용·자기회귀·커스텀 제약 로직). 이 다양한 조합을 다시 벤치마크한 결과, 넷플릭스는 vLLM을 '표준 경로 엔진(paved-path engine)'으로 낙점했다.
✓
vLLM을 고른 4가지 이유
① 컴파일 없이 커스텀 모델 구조를 바로 로딩 → 빠른 반복. ② 커스텀 디코딩 로직을 위한 확장 훅(3부의 핵심). ③ 실패와 중간 상태를 들여다보기 쉬운 디버깅성. ④ 연구자들이 이미 vLLM에 익숙해 연구→프로덕션 이관 비용이 낮음.
핵심은 "이제 최고 성능이 아니라 운영 적합성(operational fit)을 택할 수 있게 됐다" 는 것이다. 성능 격차가 사라진 세상에서는, 반복 속도·유연성·디버깅성이 몇 %의 속도보다 값지다. 아래에서 두 엔진의 트레이드오프를 직접 만져보자.
Triton에 vLLM을 끼우는 두 가지 방법
엔진을 골랐으니, 이제 모델을 어떻게 포장(packaging)해서 Triton에 넣느냐가 문제다. Triton은 '백엔드'라는 플러그인 구조로 여러 프레임워크를 지원하는데, vLLM을 넣는 길이 두 갈래다. 이 선택이 유지보수에 큰 영향을 준다.
포장 방식
Python 백엔드
vLLM 백엔드
입출력 명세
포장 시점에 텐서 스펙을 손으로 못 박음 (동결)
config가 가리키는 대로 배포 시점에 자동 생성
정체
가중치·토크나이저를 가리키는 JSON 설정 한 장
가중치·토크나이저를 가리키는 JSON 설정 한 장
프런트엔드 업그레이드
I/O가 바뀔 때마다 포장 코드도 함께 고쳐야 함
모델과 프런트엔드가 독립적으로 진화
용도
커스텀 전·후처리, 앙상블 등 탈출구
표준 HuggingFace 모델의 기본 경로
넷플릭스의 판단은 명확하다. vLLM 백엔드가 '아키텍처적으로 올바른 기본값' 이다. 모델 작성자는 텐서 스펙을 정의할 필요 없이 설정 파일만 넘기면 되고, 모델과 프런트엔드가 서로 발을 밟지 않는다. Python 백엔드는 커스텀 전·후처리가 꼭 필요한 소수 모델을 위한 탈출구로 남긴다.
그런데 프로덕션에서 물렸다: 버전 핀 사고
"올바른 기본값"에도 함정이 있었다. Triton의 vLLM 백엔드는 특정 vLLM API 버전에 맞춰 컴파일돼 있다. 둘이 어긋나면 백엔드가 아예 로딩되지 않는다. 넷플릭스가 겪은 실제 사례:
버전 불일치 = 배포 전면 실패
Triton 25.09 가 vllm.engine.metrics 모듈을 import 시도
→ 그런데 이 모듈은 vLLM 0.11.2 에서 제거됨
→ 백엔드 로딩 실패 → 배포 전체 중단
교훈은 명확했다. 플랫폼이 서비스 이미지를 구울 때 호환되는 버전들을 함께 테스트하고 못 박아야(pin) 하며, 모델 작성자가 포장 단계에서 vLLM 버전을 마음대로 바꾸지 못하게 막아야 한다. 화려한 설계보다 이런 '버전 관리'의 디테일이 프로덕션의 생사를 가른다.
모두가 같은 언어를 쓴다: OpenAI 호환 API
1부에서 예고한 두 진입로의 비밀이 여기서 풀린다. 넷플릭스의 대원칙은 "LLM은 특별한 눈송이가 아니다" 였다. 모든 모델은 똑같은 gRPC로 서빙된다. 그런데 왜 LLM 앱에는 REST HTTP 진입로를 하나 더 뚫었을까?
답은 OpenAI 호환 API가 사실상 업계 표준이 됐기 때문이다. 추론 엔진(vLLM·SGLang·TGI), 오케스트레이션 프레임워크(LangChain·LlamaIndex), 평가 도구, 클라이언트 라이브러리 — LLM 생태계 전체가 OpenAI의 /v1/chat/completions 형태를 공용어로 쓴다. 그래서 넷플릭스는 gRPC 옆에 OpenAI 호환 API를 추가 프런트엔드로 노출했다.
$
진짜 보상: 실험 → 프로덕션이 매끄러워진다
개발자가 호스티드 모델로 프로토타입을 만들다가, 품질·지연·비용·프라이버시 때문에 자체 호스팅한 파인튜닝 모델로 갈아탈 때 — API가 같으니 코드를 거의 안 바꾼다. base_url과 모델 이름만 바꾸면 끝. 1부에서 본 Uber의 GenAI Gateway가 "OpenAI API를 흉내 낸다"고 한 것도 정확히 같은 이유다.
여기서도 프로덕션이 물렸다: 조용히 사라진 response_format
넷플릭스는 이 프런트엔드를 NVIDIA Triton이 제공하는 OpenAI 호환 프런트엔드를 재사용해 구현했다. 그런데 도입하자마자 구멍 하나가 드러났다.
무음 실패 (silent failure)
증상
호출자가 response_format으로 "JSON으로 답해줘"를 요청 → 이 필드가 vLLM에 닿기 전에 조용히 버려짐 → 아무 제약 없이 생성 → 깨진 JSON이 반환되는데 플랫폼은 에러조차 안 냄
'무음 실패'는 가장 위험한 종류의 버그다. 에러가 나면 알기라도 하는데, 조용히 잘못된 값을 뱉으면 한참 뒤에야 발견된다. 그리고 이 response_format이 바로 3부의 주인공인 제약 디코딩으로 이어지는 다리다.
무중단으로 새 버전을 내보내는 법
모델을 서빙하다 보면 새 버전을 계속 배포해야 한다. GPU 배포는 CPU 서비스보다 까다롭다. 모델 가중치가 수 GB~수백 GB라 켜지는 데 오래 걸리고(콜드 스타트), 버전 간 입출력 스키마가 바뀌기도 한다. 넷플릭스는 두 가지 전략을 쓴다.
전략
Red-Black
Versioned
방식
새 버전을 나란히 띄우고 트래픽을 단계적으로 이동
(모델ID, 버전) 쌍마다 독립 배포를 유지
동시 서빙
전환 중 잠깐 겹침 (신↑ 구↓)
여러 버전이 계속 공존
스키마 변경
구 소비자가 신 배포에 '옛' 요청을 보내 실패
소비자가 준비될 때까지 구버전이 계속 응답
비용
저렴 (인터페이스 안정 시 최적)
전환 기간 GPU 비용 일시 증가
롤백
실패 시 원자적 롤백
구버전이 살아있어 안전
Red-Black은 인터페이스가 안정적일 때의 기본값이다. 새 버전이 상태 점검을 통과하면 트래픽을 넘기고, 문제가 생기면 통째로 되돌린다. 하지만 입출력 스키마가 바뀌는 경우(예: 텐서 차원이 달라짐), 상류 소비자는 새 모델이 완전히 뜨기 전엔 설정을 못 바꾸므로 전환 구간에 '옛' 요청이 '새' 배포로 흘러가 실패한다. 이 빈틈을 메우는 게 Versioned다. 버전마다 독립 배포를 유지해, 소비자가 준비된 뒤 스스로 갈아타게 한다. 대신 전환 기간 동안 GPU를 두 벌 쓰는 비용을 감수한다.
넷플릭스의 권고: 가능하면 텐서 모양 같은 가변 설정을 모델 안에 심어 버전 무관하게 만들어, 값싼 Red-Black 경로를 타라. Versioned는 인터페이스 파괴가 불가피한 드문 경우를 위해 아껴둔다.
두 전략이 실제로 어떻게 다르게 흘러가는지, 아래에서 직접 배포를 굴려보자.
켜지는 순간과 계기판: 마지막 디테일
원문은 설계 결정 외에 운영 디테일 두 가지를 더 짚는다. 둘 다 "설계 단계에선 예상 못 한 구멍"이었다.
🚀 부팅 시퀀스 거대한 모델을 S3나 허깅페이스에서 켤 때마다 내려받으면 콜드 스타트가 스케줄러 한계를 넘긴다. → 모델 공지 시점에 Amazon FSx(고성능 파일시스템)에 미리 올려둬 웜 스타트가 오브젝트 스토리지 대신 빠른 파일시스템을 때리게 한다.
📊 통합 메트릭 vLLM은 디스크 파일로, Triton은 자체 엔드포인트로 메트릭을 뱉는다. 서로 모른다. Triton 기본 브리지는 40여 개 vLLM 지표 중 9개만 노출 — 토큰 처리량·KV 캐시 사용률·프리픽스 캐시 적중률 같은 결정적 지표가 빠진다. → 둘을 합쳐 하나의 /metrics로 내보내는 경량 프록시를 붙였다.
특히 두 번째의 세 지표는 LLM 서빙의 '계기판'이라 알아둘 만하다. 토큰 처리량(초당 몇 토큰), KV 캐시 사용률(1부의 그 KV 캐시가 얼마나 찼나 — 1.0에 가까우면 요청을 밀어내야 함), 프리픽스 캐시 적중률(공유 프롬프트 재활용이 얼마나 잘 되나). 이 계기판 없이 LLM 서빙을 운영하는 건 눈 감고 운전하는 것과 같다.
2부 요약
넷플릭스 아키텍처: 통합 서빙 시스템(JVM) → MSS → Triton(관리) + vLLM(추론). 작은 모델은 그 자리서, 큰 모델은 GPU로 위임.
KV 캐시는 자기회귀 생성의 필수 저장고지만 메모리를 잡아먹는다. PagedAttention은 이걸 OS 페이징처럼 블록 단위로 관리해 낭비를 없애고 처리량을 2~4배로.
연속 배칭은 식당 주방처럼 빈자리를 즉시 채워 GPU를 놀리지 않는다(Orca, OSDI 2022).
추론은 프리필(연산 병목) + 디코드(메모리 병목) 두 얼굴. 청크 프리필로 둘을 섞는다.
넷플릭스는 TensorRT-LLM → vLLM으로 갈아탔다. 최고 성능이 아니라 운영 적합성(반복·유연·디버깅·확장) 때문.
vLLM 백엔드(설정 구동)가 기본, Python 백엔드는 탈출구. 버전 핀을 안 하면 배포가 통째로 죽는다.
OpenAI 호환 API가 표준어라 실험→프로덕션이 매끄럽다. response_format 무음 드롭 같은 함정도 있었다.
무중단 배포는 Red-Black(기본)과 Versioned(스키마 파괴용). 부팅 캐싱과 통합 메트릭이 마지막 디테일.
3부에서 계속 → — 이제 넷플릭스 이야기의 클라이맥스다. "모델이 반드시 유효한 JSON만 뱉게 만들라"는 요구가, 어떻게 규모의 벽에 부딪혔고, vLLM V0에서 V1로 넘어가며 어떻게 그 벽을 넘었는지 — 제약 디코딩(constrained decoding) 의 세계로 들어간다.
참고 자료
· Netflix TechBlog, "In-House LLM Serving at Netflix" (2026-07-17) / InfoQ 정리본 (2026-07)
· Kwon et al., "Efficient Memory Management for LLM Serving with PagedAttention," SOSP 2023 (arXiv:2309.06180)
· Yu et al., "Orca: A Distributed Serving System for Transformer-Based Generative Models," OSDI 2022
· vLLM 공식 블로그/문서, NVIDIA Triton·TensorRT-LLM 문서
· 처리량 배수(2~4× 등)는 각 원 논문이 보고한 수치이며 워크로드에 따라 달라진다.