Skip to main content
Views 2

Nano-vLLM로 배우는 vLLM 추론 엔진 작동 원리 한눈에 정리

Summary

Nano-vLLM로 배우는 vLLM 추론 엔진 작동 원리 한눈에 정리

LLM을 “서비스”로 올리는 순간, 모델 자체보다 더 중요해지는 게 있습니다. 바로 추론 엔진(inference engine)입니다. 우리가 쓰는 대부분의 LLM API는 결국 “요청을 모으고, GPU를 아껴 쓰고, 토큰을 빠르게 뽑아 주는” 이 엔진 위에서 돌아갑니다.

이번 글에서는 약 1,200줄 파이썬 코드로 vLLM 스타일 핵심을 담아낸 Nano-vLLM을 통해, 추론 엔진이 실제로 어떤 구조로 움직이는지 스토리처럼 풀어보겠습니다. 프롬프트가 들어와 토큰이 되고, 배치로 묶이고, KV 캐시를 재활용하며, Prefill/Decode를 거쳐 응답이 나가기까지의 전체 흐름을 잡는 것이 목표입니다.1

Nano-vLLM이 흥미로운 이유: “필수 기능만 남긴” 미니 추론 엔진

vLLM은 실서비스에서 널리 쓰이는 추론 엔진 계열로, “많은 요청을 GPU에 잘 태우는 법”에 특화되어 있습니다. 다만 코드는 방대하고 컴포넌트도 많아, 처음 보는 사람에겐 구조가 잘 안 들어올 때가 많습니다.

Nano-vLLM은 그 핵심을 경량으로 재구성한 버전입니다. 프리픽스 캐싱, 텐서 병렬화, CUDA Graphs, torch compile 최적화 같은 실전 기능을 갖추면서도, 읽고 이해할 만한 분량으로 정리되어 있어 “추론 엔진 설계의 본질”을 공부하기에 좋습니다.1

재미있는 포인트는 성능입니다. 벤치마크에서 정식 vLLM과 비슷하거나 약간 더 높은 처리량(throughput)도 보여줘서, “코드가 길어야만 빠른 게 아니다”라는 힌트를 줍니다.1

프롬프트가 들어오면: 토크나이저가 먼저 “입장권(토큰)”을 나눠준다

사용자가 “요약해줘”라고 입력하면, 엔진은 이 문장을 바로 GPU로 던지지 않습니다. 먼저 토크나이저(tokenizer)가 문장을 토큰 단위로 쪼개고, 각 토큰을 정수 ID로 바꿉니다. 이 ID들의 배열이 바로 시퀀스(sequence)입니다.

여기서 실무자들이 종종 놀라는 지점이 있습니다. 똑같은 문장이라도 모델이 다르면 토큰으로 쪼개지는 방식이 달라서, 토큰 개수 자체가 달라질 수 있다는 점입니다.1 같은 길이의 프롬프트처럼 보여도 어떤 모델에서는 80토큰, 다른 모델에서는 110토큰이 될 수 있고, 이것이 곧 비용·지연·메모리 사용량에 영향을 줍니다.

add_request와 Scheduler: “혼자 태우지 말고, 카풀로 태우자”

이제 요청이 들어오면 add_request가 바로 계산하지 않고, 일단 대기열에 넣어둡니다. 그다음 Scheduler가 등장합니다. Scheduler의 역할은 간단히 말해 “카풀 매칭”입니다. 여러 사용자의 시퀀스를 적당히 모아 배치(batch)를 만들고, GPU가 가장 효율적으로 일할 순간에 한 번에 태워 보냅니다.1

이 배치가 중요한 이유는 GPU의 고정 오버헤드(커널 런치, 준비 작업 등)를 여러 요청이 나눠 부담하게 만들어 처리량을 끌어올리기 때문입니다.1

다만 대가는 있습니다. 카풀을 기다리느라 개별 요청의 대기시간(지연, latency)이 늘어날 수 있습니다.1 그래서 요즘 커뮤니티에서도 “이제는 무조건 처리량만 보던 시대에서, 콜드 스타트와 지연을 더 신경 쓰는 분위기”가 보인다는 이야기가 나옵니다.2 결국 서비스 성격(챗봇/배치요약/에이전트/검색 등)에 따라 최적점이 달라집니다.

Prefill vs Decode: 추론이 “한 번에 읽고, 한 글자씩 쓴다”

추론은 크게 두 국면으로 나뉩니다.

먼저 Prefill 단계에서는 사용자가 준 입력 토큰 전체를 한 번에 처리합니다. “문제를 쭉 읽는 시간”이라고 생각하면 쉽습니다.1

그다음 Decode 단계에서는 새로운 토큰을 하나씩 생성합니다. 이때는 “한 글자(정확히는 한 토큰)씩 답안을 써 내려가는 시간”입니다.1

서비스가 느려지는 순간을 떠올려보면 대개 Decode 쪽이 길게 느껴집니다. 토큰이 생성될 때마다 반복적으로 연산이 일어나기 때문입니다. 그래서 엔진들은 Decode를 얼마나 효율적으로 굴리느냐에 성능이 갈립니다.

KV 캐시와 Block Manager: “이미 읽은 페이지는 다시 읽지 말자”

LLM은 토큰을 하나 생성할 때마다 이전 문맥을 참고해야 합니다. 이때 매번 처음부터 다시 계산하면 너무 느리니, 중간 결과를 KV 캐시로 저장해두고 재사용합니다. Nano-vLLM에서는 이 관리를 Block Manager가 맡습니다.1

핵심 아이디어는 “고정 크기 블록(기본 256토큰)” 단위로 잘라 관리하는 것입니다.1 블록 단위로 메모리를 할당하고 재사용하면, 새 요청이 들어와도 필요한 부분만 이어 붙이듯 쓰기 쉬워집니다.

여기서 프리픽스 캐싱(prefix caching)이 빛납니다. 여러 요청이 같은 프롬프트 앞부분(예: 시스템 프롬프트, 공통 지시문)을 공유한다면, 그 블록을 해시로 식별해 중복 계산 없이 캐시를 그대로 재활용할 수 있습니다.1 실서비스에서 “시스템 프롬프트가 긴 챗봇”일수록 체감 효과가 큽니다.

또 하나의 엔지니어링 포인트는 제어와 데이터의 분리입니다. 블록의 메타데이터는 CPU에서 관리하고, 실제 KV 텐서는 GPU에 둬서 효율적으로 핸들링합니다.1

Model Runner, 텐서 병렬화, CUDA Graphs: GPU를 ‘덜 흔들고’ 더 빠르게

실제 연산은 Model Runner가 수행합니다. 그리고 모델이 크거나 GPU가 여러 장이면 텐서 병렬화(Tensor Parallelism, TP)로 여러 GPU에 일을 나눕니다. Nano-vLLM은 리더-워커 패턴으로 이 병렬 처리를 지원합니다.1

또 하나의 “속도 비밀병기”가 CUDA Graphs입니다. 자주 등장하는 배치 크기별로 GPU 실행 흐름을 미리 캡처해두고, 다음부터는 재생하듯 돌려 커널 실행 오버헤드를 줄입니다.1 “매번 새로 세팅하지 말고, 자주 쓰는 동작은 단축키로” 같은 느낌입니다.

마지막으로 샘플링도 빼놓을 수 없습니다. 같은 확률 분포에서 어떤 토큰을 뽑느냐가 답변의 성격을 바꾸는데, 온도(temperature)가 낮으면 더 결정론적이고, 높으면 더 다양한 결과가 나옵니다.1 엔진은 이 선택을 매 Decode 스텝마다 수행합니다.

시사점 내용 (핵심 포인트 정리 + 개인적인 생각 또는 실용적 조언)...

Nano-vLLM을 따라가다 보면 “추론 엔진은 모델의 바깥에서 벌어지는 시스템 게임”이라는 감각이 생깁니다. 토큰화부터 배치 전략, Prefill/Decode 분리, KV 캐시의 블록화와 프리픽스 재사용, 그리고 CUDA Graphs 같은 런타임 최적화까지—이 조합이 결국 처리량과 지연을 결정합니다.1

실무적으로는 한 가지 질문으로 귀결됩니다. “우리 서비스는 카풀을 더 할 것인가, 아니면 바로 출발할 것인가?” 처리량(throughput)을 끌어올리는 설계는 대개 지연(latency)을 희생하고, 반대도 마찬가지입니다.12 Nano-vLLM의 내부 구조를 이해하면, 이 트레이드오프를 감이 아니라 근거로 조정할 수 있습니다.

다음 편(Part 2)에서는 모델 내부 연산과 어텐션, KV 캐시 메모리 구조, Dense/MoE 차이, 더 깊은 병렬화까지 들어간다고 하니, 이번 글을 “지도”로 삼아 읽으면 훨씬 덜 헤맬 겁니다.1

참고

1Nano-vLLM: vLLM 스타일의 추론 엔진이 어떻게 작동하는지

2r/LocalLLaMA on Reddit: vLLM raising $150M confirms it: We have moved from the "Throughput Era" to the "Latency(Cold Starts)."

Nano-vLLM로 배우는 vLLM 추론 엔진 작동 원리 한눈에 정리

이 노트는 요약·비평·학습 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.