LLM을 사내 서비스에 넣으면 모델 자체보다 배포와 요청 처리에서 더 많은 결정이 생긴다. 이미 대규모 추천 인프라를 운영하던 Netflix도 예외는 아니었다.

LLM도 하나의 대형 모델일 뿐

Netflix는 LLM 전용 시스템을 따로 만들지 않았다. 기존 JVM 기반 플랫폼이 이미 라우팅, A/B 테스트, 피처 조회, 추론, 로깅을 맡고 있었기 때문이다. 작은 모델은 JVM의 CPU에서, 큰 모델은 GPU 기반 Model Scoring Service에서 돌렸다. LLM도 이 경로에 올릴 대형 모델의 하나로 봤다.

그런 대형 모델 중 하나인 GenRec은 Netflix의 LLM을 시청 이력과 카탈로그 데이터에 맞춰 훈련한 추천 랭커다. 두 단계 훈련을 거치며 오프라인 랭킹 지표가 각각 10~20%, 35~50% 개선됐다. 사내 데이터와 독자적인 실행 방식이 결합돼 외부 API로 대체하기 어려웠다.

GenRec은 토큰을 하나씩 생성하지 않는다. 프롬프트와 후보 목록을 받아 한 번의 forward pass로 점수를 매긴다. 디코딩 로직을 직접 다뤄야 하는 만큼, Netflix가 TensorRT-LLM 대신 vLLM을 택한 이유도 최고 속도보다는 모델과 디코딩 로직을 고치기 쉬웠기 때문이다.

이 구성이 굴러갈 수 있었던 건 배포, 관측, GPU 스케줄링을 담당할 기반이 이미 있었기 때문이다. 사용량에 따라 외부 API가 더 저렴하지 않을까. 그래도 Netflix는 새로 만들어야 할 범위가 작았다.


배치에서 드러난 병목

초기에는 Python 로짓(logit, 모델이 다음 토큰 후보에 매기는 점수) 프로세서로 출력 형식을 제한했다. GPU가 로짓을 계산한 뒤 CPU가 요청별 규칙을 검사하는 구조였다. 정확성에는 문제가 없었지만 Python의 GIL(전역 인터프리터 락) 때문에 병렬 처리가 어려웠고 배치가 커질수록 p99 지연(요청의 99%가 처리되는 시간)이 늘었다.

이 문제는 단일 요청 테스트에서는 드러나지 않았는데, vLLM 이슈에는 요청 하나에 0.7초 걸리던 처리가 배치 크기 50에서 약 35초까지 늘어난 사례 가 있다. GPU 연산은 배치로 빨라져도 CPU 검사는 요청 수만큼 반복되므로, 동시 요청이 없는 벤치마크에서는 이 병목이 드러나겠는가.

이 병목을 줄이기 위해 vLLM V1은 로짓 처리를 배치 단위 API로 바꿨다. Netflix도 프로세서를 다시 만들고 연산이 몰리는 부분을 C++ 멀티스레딩으로 옮겼다. 그 결과 배치가 커져도 처리 시간이 크게 늘지 않았다.

대신 상태 관리가 복잡해졌다. 요청이 배치에 들어오고 나가는 과정을 직접 추적해야 했고, 메모리 부족으로 KV 캐시(추론 중 토큰별 연산 결과를 저장해두는 메모리)가 비워져 요청이 다시 실행되는 상황도 처리해야 했다. Netflix는 토큰 기록이 줄어들면 상태 머신을 초기화해 제약 상태를 복구했다. 커스텀 디코딩 운영의 책임은 정확성뿐일까. 배치 성능과 상태 복구까지 범위에 넣는다.


전달되지 않은 response_format

NVIDIA Triton의 OpenAI 호환 프론트엔드는 response_format을 입력으로 받았지만 vLLM에는 넘기지 않았다. 호출자는 JSON 출력을 요청했다고 생각했지만 모델은 제약 없이 문장을 만들었고 오류도 남지 않았다. Netflix는 프론트엔드를 패치해 이 필드를 vLLM의 guided decoding 인자로 전달했다.

그래도 OpenAI 호환 HTTP는 사내 gRPC와 함께 유지했다. 외부 모델로 실험하던 팀이 사내 모델로 옮길 때 호출 코드를 거의 바꾸지 않아도 됐기 때문이다. 다만 API 모양이 같다고 동작까지 같을까. 받은 필드가 실제 추론 엔진까지 전달되는지는 통합 테스트로 가려진다.

호출 경로뿐 아니라 패키징에서도 호환을 고정했다. vLLM 백엔드를 기본으로 쓰고 비표준 모델에만 Python 백엔드를 허용했다. Triton과 vLLM의 API가 어긋나면 로드에 실패해 검증된 버전 조합도 고정했다.

모델을 올리는 방식도 입출력 스키마에 따라 나눴다. 인터페이스가 같으면 Red-Black 방식으로 트래픽을 조금씩 넘겼다. 호환되지 않는 변경은 (modelId, version)별로 따로 배포했다. 여러 버전이 동시에 GPU를 차지하므로 꼭 필요할 때만 썼다.

기동 시간도 줄였다. 모델을 Amazon FSx에 미리 저장해 시작할 때 내려받지 않도록 했다. FSx for Lustre와 GPUDirect(스토리지와 GPU 메모리를 CPU 경유 없이 직접 연결하는 기술)를 이용한 AWS 시험에서는 405B 모델의 로드 시간이 약 18분에서 6초로 줄었다. 관측 경로도 하나로 모았다. Triton과 vLLM의 메트릭은 프록시를 거쳐 하나의 /metrics로 모았다.


자체 서빙을 선택할 때

자체 서빙은 모델의 생성 과정에 직접 개입해야 할 때 가치가 크다. 커스텀 제약 디코딩, 비표준 모델 구조, 디코딩 없는 추천 랭킹은 일반적인 호스티드 API만으로 구현하기 어렵다. 배포 시점과 버전 운영, 장애 관측 방식도 직접 통제할 수 있다.

토큰 단위 개입이 제품의 핵심 기능인지, 배포, 오토스케일링, 관측을 맡을 플랫폼이 이미 있는지, 사용량이 자체 운영의 손익분기점을 넘는지가 결정을 가른다. 플랫폼이 없다면 모델보다 기반 시스템을 만드는 일이 더 커진다. 반대로 토큰 개입이 꼭 필요하다면 사용량이 적어도 자체 서빙을 택할 이유가 생긴다.

실제로 Netflix가 떠안은 일도 대부분 운영에 가깝다. C++ 로짓 처리, 패치한 프론트엔드, 메트릭 프록시, 버전별 배포, FSx 사전 적재, Triton, vLLM 호환표를 계속 관리해야 한다. 자체 서빙 비용에는 GPU뿐 아니라 상류 프로젝트가 바뀔 때마다 패치와 버전을 검증할 인력도 포함된다.

마지막으로, 비용표만 보고 끝낼 일일까. 실제 배치 부하의 p99 지연을 먼저 살핀다. 단일 요청 처리량만으로는 GIL 대기, 상태 복구, KV 캐시 선점, 버전 전환 비용이 보이지 않는다. 자체 서빙의 경제성은 트래픽이 붙은 뒤의 성능과 앞으로의 유지보수까지 포함해 계산된다.

그 점에서 Netflix의 GenRec도 실험 단계에서 쓰던 시스템과 실제 트래픽을 받는 시스템은 달랐다. 주변 소프트웨어가 바뀌면 처음에 세운 선택 기준도 함께 달라진다.


#언어모델#기업 AI