vLLM 배치 사이즈부터 모델 스왑까지 직접 운영

공식 문서는 설치까지는 친절하다. 그 다음부터가 문제다.

vLLM 깃허브 star 수는 이미 수만을 넘었고, 한국어로 된 설치 가이드도 제법 나온다. 그런데 막상 추론 서버를 실제 운영 환경에 올려보면 공식 문서가 다루지 않는 영역이 금방 나타난다. GPU 메모리 설정을 어떻게 잡아야 안정적으로 돌아가는지, 배치 사이즈가 응답 시간에 어떻게 영향을 주는지, 모델을 스왑해야 할 때 서비스를 얼마나 끊어야 하는지 같은 것들이다.

이 글은 그 부분을 채우려고 쓴다. 이론보다 내가 직접 겪은 실수와, 그 실수에서 찾아낸 기준을 중심으로 정리했다. vLLM을 막 띄워본 사람보다는 실제로 운영하면서 “뭔가 이상하다”는 느낌이 드는 사람에게 더 유용할 것이다.

처음 서버를 올릴 때 가장 많이 틀리는 설정

--gpu-memory-utilization 파라미터 얘기다. 기본값이 0.9인데, 처음 올릴 때 이걸 그냥 놔두면 나중에 당황하는 경우가 생긴다.

기본값 0.9는 GPU 메모리의 90%를 vLLM이 쓰겠다는 뜻이다. 문제는 이 90%가 모델 가중치와 KV 캐시를 합친 값이라는 점이다. 모델 가중치가 크면 KV 캐시에 남는 메모리가 줄어들고, KV 캐시가 부족하면 동시에 처리할 수 있는 요청 수가 줄어든다. 요청이 몰리면 큐가 쌓이고, 응답 시간이 기하급수적으로 늘어난다.

내가 처음 70억 파라미터급 모델을 A100 80GB에 올렸을 때, 0.9 기본값으로 설정했더니 단일 요청에서는 아무 문제가 없었다. 그런데 동시 요청이 10개를 넘어가니까 레이턴시가 뚝 뛰었다. KV 캐시가 빠르게 채워지면서 새 요청이 기다리는 시간이 늘어난 거였다. 모델 크기에 비해 GPU 여유가 없는 상황이었는데 그걸 처음에 못 봤다.

기준을 세우면 이렇다. 단일 GPU에서 모델 하나만 올릴 때는 0.85~0.88 정도가 실무에서 더 안정적이었다. 0.9는 이론상 최대치고, 실제 운영에서는 약간의 여유를 두는 게 낫다. 멀티 GPU tensor-parallel로 샤딩할 때는 각 GPU의 메모리 사용량을 같은 기준으로 잡되, 노드 간 통신 오버헤드를 고려해서 더 보수적으로 잡는 게 맞다.

배치 사이즈 – 높을수록 좋다는 건 절반만 맞는 말이다

vLLM의 핵심 강점 중 하나가 continuous batching이다. 요청이 들어오는 대로 실시간으로 배치를 구성해서 GPU 활용률을 높이는 방식이다. --max-num-seqs 파라미터로 동시에 처리할 수 있는 최대 시퀀스 수를 설정한다.

이걸 높게 잡으면 처리량(throughput)은 올라간다. 그런데 개별 요청의 응답 시간(latency)은 길어진다. 배치 안에 요청이 많아질수록 각 요청이 기다리는 시간도 늘어나기 때문이다.

내가 운영하는 RAG 파이프라인에서는 이게 중요한 트레이드오프였다. 사용자가 직접 대화하는 엔드포인트는 응답 시간이 중요하다. 2~3초 안에 첫 토큰이 나와야 자연스럽다. 반면 배치로 문서를 처리하는 백엔드 파이프라인은 응답 시간보다 처리량이 중요하다. 밤새 돌리면 되니까.

이 두 가지를 같은 vLLM 서버 하나로 처리하려다가 둘 다 만족스럽지 않은 상황이 생겼다. 결국 엔드포인트 용도에 따라 서버를 분리했다. 사용자 대화용 서버는 --max-num-seqs를 낮게 잡고 응답 시간을 우선했고, 배치 처리용 서버는 높게 잡아서 처리량을 극대화했다. 인프라 비용이 올라가는 대신 두 용도 모두에서 기대하는 성능이 나왔다.

모델 스왑 – 서비스 중단 없이 바꾸는 방법

모델을 업데이트해야 할 때 어떻게 하느냐는 운영에서 꼭 부딪히는 문제다.

단순하게 생각하면 서버 내리고 새 모델 올리면 된다. 그런데 그 사이에 들어오는 요청은 어떻게 하나. 프로덕션에서 다운타임을 허용하는 서비스는 많지 않다.

내가 쓰는 방법은 블루-그린 배포다. 새 모델로 완전히 별도 인스턴스를 올려서 준비 상태를 확인한 다음, 로드밸런서에서 트래픽을 새 인스턴스로 전환한다. 기존 인스턴스는 처리 중인 요청이 완료될 때까지 남겨두고 그다음에 내린다.

GPU 인스턴스가 하나뿐일 때는 이렇게 하기 어렵다. 이 경우에는 요청을 잠시 큐에 쌓아두고 모델 스왑 시간을 최소화하는 쪽으로 간다. 70억 파라미터 모델 기준으로 A100에서 로딩 시간이 보통 30초~1분 사이다. 이 시간을 큐 타임아웃 안에 넣을 수 있으면 사용자 체감 중단 시간을 크게 줄일 수 있다.

모델 파일 자체를 NVMe SSD에 미리 올려두는 것도 로딩 시간을 줄이는 방법이다. 네트워크 스토리지에서 로딩하는 것보다 체감이 다르게 빠르다. Azure에서 운영할 때는 로컬 임시 디스크에 모델을 미리 캐시해두는 방식을 썼고, 이게 모델 스왑 다운타임을 줄이는 데 실질적으로 도움이 됐다.

Claude Code 제안을 따랐다가 서버가 뻗은 경험

솔직한 실패담도 하나 넣는다.

Claude Code로 vLLM 설정 파일을 리팩토링하던 중에, tensor-parallel-size 관련 설정 수정 제안을 그대로 따랐다가 추론 서버가 죽은 적이 있다. Claude Code가 제안한 설정이 코드 레벨에서는 문법적으로 맞았는데, 실제 GPU 토폴로지와 NVLink 연결 구조를 고려하지 않은 값이었다.

GPU 사이의 물리적 연결 구조가 tensor-parallel 샤딩에 큰 영향을 준다. NVLink로 직접 연결된 GPU 쌍은 tensor-parallel에 효율적이지만, PCIe만으로 연결된 GPU를 같은 tensor-parallel 그룹에 넣으면 통신 오버헤드가 커진다. 이 부분은 AI 코딩 도구가 하드웨어 토폴로지 정보를 모르기 때문에 제안의 정확도가 낮다. 인프라 레이어에서는 AI 코딩 도구 제안을 참고 수준으로만 쓰고, 실제 적용 전에 반드시 하드웨어 스펙과 교차 확인하는 과정이 필요하다는 걸 이때 제대로 배웠다.

운영 모니터링 – 어디를 봐야 하는가

vLLM은 기본적으로 Prometheus 메트릭을 내보낸다. /metrics 엔드포인트를 열어두면 Grafana에서 바로 붙일 수 있다.

내가 실제로 중요하게 보는 지표는 세 가지다. 첫 번째는 vllm:num_requests_running이다. 현재 GPU에서 실제로 처리 중인 요청 수인데, 이게 max-num-seqs에 가까워지면 곧 레이턴시가 올라간다는 신호다. 두 번째는 vllm:gpu_cache_usage_perc다. KV 캐시 사용률인데, 이게 90%를 넘어가기 시작하면 새 요청 처리가 느려진다. 세 번째는 vllm:time_to_first_token_seconds다. 사용자가 체감하는 응답 시간과 직결되는 지표라서 이게 갑자기 올라가면 뭔가 막힌 곳이 있다는 신호다.

이 세 가지에 알림을 걸어두면, 문제가 터지기 전에 미리 감지하고 대응할 수 있다. 사후 대응보다 이 알림 체계를 먼저 만드는 게 훨씬 마음이 편하다.

결국 vLLM 운영이 가르쳐준 것

설치하고 모델 하나 올려서 동작 확인하는 것까지는 누구나 할 수 있다. 진짜 운영은 그다음부터다. GPU 메모리를 얼마나 남겨둬야 동시 요청이 몰렸을 때 버티는지, 용도에 따라 서버를 어떻게 나눠야 하는지, 모델을 바꿀 때 다운타임을 어떻게 줄이는지 — 이 판단들이 쌓여서 안정적인 추론 서버가 된다.

공식 문서가 다루지 않는 이 영역은 결국 직접 운영해보면서 익힐 수밖에 없다. 그 과정에서 겪은 실수들이 결국 가장 좋은 설정 기준이 된다. 이 글이 그 시행착오를 조금이라도 줄이는 데 도움이 됐으면 한다.

Leave a Comment