상태: 초안 (2026-10-27), 풀링·멀티모달 실측, 병렬화 미실측. 풀링 측정은 8GB급 노트북 GPU(Ada 세대) 한 장, vLLM 0.31.0 으로 2026-10-27 에 했습니다. 병렬화와 멀티모달은 미실측이며, 공식 문서(0.32.0)와 논문에서 확인한 내용만 적었습니다. LoRA 다중 서빙의 실측은 「토스 ML/AI Infra 준비」 15편에 있습니다. 2026-10-27 추가. 승인을 받아 vLLM 0.32.0 이미지와 Qwen3-VL-2B-Instruct(Apache-2.0, safetensors 4.26GB)를 받아 2-1절의 멀티모달 실측을 더했습니다. 처음 판에서 「미실측」이던 내용입니다. 작성 규칙: 실측 데이터가 기본이고, 못 잰 것은 「미실측」, 확인하지 못한 것은 「미확인」으로 표시합니다.
목차
- 1. 병렬화 네 가지: 무엇을 나누는가
- 2. 멀티모달 입력
- 2-1. 2026-10-27 실측: 8GB 에 2B 시각 언어 모델 올리기
- 3. 풀링: 임베딩과 재순위도 같은 엔진으로
- 4. LoRA 다중 서빙
- 5. 확인하지 못한 것
- 참고
1. 병렬화 네 가지: 무엇을 나누는가
| 방식 | 옵션 | 나누는 것 | 통신 | 쓰는 때 |
|---|---|---|---|---|
| 텐서 병렬(TP) | --tensor-parallel-size |
한 층의 행렬을 GPU 마다 조각으로 | 층마다 all-reduce | 모델이 GPU 한 장에 안 들어갈 때, 한 서버 안에서 |
| 파이프라인 병렬(PP) | --pipeline-parallel-size |
층을 앞뒤로 나눠 GPU 마다 몇 층씩 | 단계 사이 활성값만 | 서버 여러 대에 걸칠 때, NVLink 가 없을 때 |
| 데이터 병렬(DP) | --data-parallel-size |
나누지 않고 모델을 통째로 여러 벌 | 거의 없음(MoE 는 있음) | 처리량을 늘릴 때 |
| 전문가 병렬(EP) | --enable-expert-parallel |
MoE 의 전문가들을 GPU 마다 | 토큰을 전문가 쪽으로 all-to-all | 큰 MoE 모델 |
- TP 는 통신이 잦습니다. vLLM 은 Megatron-LM(Shoeybi 외, 2019)의 방식으로 행렬을 나누는데, 이 방식은 트랜스포머 층 하나의 순전파마다 all-reduce 를 두 번(어텐션 뒤와 MLP 뒤) 합니다. 그래서 GPU 사이가 NVLink 처럼 빠를 때 이득이 큽니다. 공식 문서도 NVLink 가 없는 GPU(예로 L40S)에서는 TP 대신 PP 를 권합니다.
- PP 는 통신이 적지만 거품이 생깁니다. 앞 단계가 끝나야 뒤 단계가 일하므로, 한 요청만 보면 GPU 들이 번갈아 쉽니다. 여러 요청을 흘려 넣어 거품을 메웁니다. 문서는 서버 안은 TP, 서버 사이는 PP 로 나누라고 권합니다(서버당 GPU 수 = TP, 서버 수 = PP).
- DP 는 KV 캐시도 따로입니다. 엔진마다 접두사 캐시가 따로라서, 같은 접두사를 같은 엔진에 보내야 캐시가 삽니다. KV 를 보고 엔진을 고르는 라우팅이 Dynamo 편의 주제였습니다. DP 에서
--max-num-seqs는 엔진마다 적용되지만--max-num-queued-reqs같은 대기열 상한은 서버 전체에 적용된다는 점도 문서에 있습니다. - MoE 는 섞어 씁니다. DeepSeek 같은 모델은 어텐션은 DP, 전문가 층은 EP 로 나누는 구성을 문서가 소개합니다.
모델이 들어가는지 볼 때는 기동 로그의 두 줄을 봅니다. GPU KV cache size: N tokens 와 Maximum concurrency for M tokens per request: Kx 입니다. 이 시리즈의 실험 서버는 「40,208토큰, 4,096토큰 요청 기준 9.82배」였습니다. 이 동시성이 목표보다 낮으면 GPU 를 늘리라고 문서는 안내합니다.
GPU 가 한 장이라 네 방식 모두 미실측입니다. 같은 계열 GPU 두 대를 1GbE 로 묶어 KV 를 옮겨 본 실측(분리 서빙이 20배 넘게 느렸음)이 「토스 ML/AI Infra 준비」 24편에 있어, 느린 연결에서 GPU 를 잇는 방식이 어떤 값을 치르는지는 그 글로 대신합니다.
2. 멀티모달 입력
vLLM 은 이미지·영상·오디오를 받는 모델을 같은 OpenAI 호환 API 로 서빙합니다. 채팅 메시지의 content 에 image_url, video_url, input_audio 조각을 넣으면, API 서버의 처리기가 미디어를 내려받고 전처리해 모델의 인코더로 보냅니다. 이미지 하나는 프롬프트 안에서 자리표시 토큰 여러 개로 바뀌고, 접두사 캐시는 이 자리표시 블록의 해시에 이미지의 해시를 섞어 같은 이미지끼리만 캐시를 나눠 씁니다(2편의 접두사 캐시 설계 문서).
운영에서 먼저 챙길 것은 성능보다 이쪽입니다.
- SSRF: 요청이 준 URL 을 서버가 대신 내려받으므로, 서버가 닿는 내부 주소를 요청자가 두드릴 수 있습니다. 문서는
--allowed-media-domains로 받을 도메인을 정하고VLLM_MEDIA_URL_ALLOW_REDIRECTS=0으로 리다이렉트를 막으라고 권합니다. 컨테이너 안에서 돌면 더 중요하다고 적혀 있습니다. - 메모리 예약: vLLM 은 모델이 받을 수 있는 최대 크기의 미디어를 기준으로 인코더 메모리를 미리 잡습니다. 쓰지 않는 입력은
--limit-mm-per-prompt '{"image":0,"video":0}'처럼 0으로 두면 그 몫을 KV 로 돌릴 수 있고, 크기 힌트(count,width,height,num_frames)로 예약을 줄일 수도 있습니다. - CPU 메모리: 같은 미디어를 다시 처리하지 않으려는 처리기 캐시가 기본 4GiB(
mm_processor_cache_gb)입니다. 컨테이너 메모리 한도를 잡을 때 이 몫을 더해야 하고, 이득이 없으면 0으로 끕니다. - 0.32.0 은 믿을 수 없는 이미지를 래스터 형식 허용 목록으로만 열도록 바꿨습니다(EPS·PostScript·WMF 거부, 릴리스 노트 기준).
처음 판에서는 시각 언어 모델을 새로 받아야 해서 미실측이었고, 승인 뒤 아래 2-1절에서 쟀습니다.
2-1. 2026-10-27 실측: 8GB 에 2B 시각 언어 모델 올리기
- 환경: 8GB급 노트북 GPU(Ada 세대) 한 장, vLLM 0.32.0, Qwen3-VL-2B-Instruct(bf16, 가중치 4.08GiB),
--max-model-len 4096. - 이미지: PIL 로 그린 PNG 를 data URL 로 넣었습니다(바깥 주소를 받지 않음). 흰 바탕에 색 사각형 하나와 네 자리 숫자. 크기 448×448 과 896×896 각 다섯 장.
- 질문: 한 이미지에 세 번. ① 「숫자가 무엇인가」(처음), ② 같은 질문 다시, ③ 「사각형의 색은」. 탐욕 디코딩.
먼저, 뜨게 만들기
| 설정 | KV 에 남은 메모리 | KV 자리 | 결과 |
|---|---|---|---|
기본(--gpu-memory-utilization 0.85) |
-0.07GiB | 기동 실패 「No available memory for the cache blocks」 | |
--limit-mm-per-prompt '{"image":1,"video":0}' 만 더함 |
-0.07GiB | 같은 실패 | |
| 메모리 비율 0.92 | 0.47GiB | 4,352토큰(4,096토큰 요청 1.06개) | 뜸 |
0.92 + 크기 힌트 {"image":{"count":1,"width":896,"height":896},"video":0} |
2.13GiB | 19,904토큰(4.86개) | 뜸 |
vLLM 은 기동할 때 「모델이 받을 수 있는 가장 큰 이미지」로 인코더를 한 번 돌려 메모리를 잽니다. 로그는 인코더 캐시 예산을 16,384토큰으로 잡았다고 적었습니다. 가중치 4GB 에 그 예산까지 얹으니 8GB 에서는 KV 자리가 남지 않았습니다. 이미지 개수만 줄여서는 소용이 없었고, 실제로 받을 이미지 크기를 알려 주자 KV 가 4.5배가 되었습니다. 작은 GPU 에서 시각 언어 모델을 띄울 때 가장 먼저 만질 손잡이입니다.
결과(크기 힌트 서버)
| 항목 | 448×448 | 896×896 |
|---|---|---|
| 프롬프트 토큰(이미지 + 질문) | 220 | 808 |
| 새 이미지의 TTFT 중앙 | 67.9ms | 238.0ms |
| 같은 이미지, 같은 질문 다시 | 31.8ms(캐시 208토큰) | 32.7ms(캐시 800토큰) |
| 같은 이미지, 다른 질문 | 32.1ms(캐시 192토큰) | 33.4ms(캐시 784토큰) |
- 이미지 토큰은 넓이에 비례했습니다. 변 길이를 두 배로 하자 토큰이 3.7배, 첫 토큰 시간이 3.5배가 되었습니다.
- 같은 이미지를 다시 보내면 인코더와 프리필을 건너뜁니다. 접두사 캐시는 이미지 해시를 블록 해시에 섞어 같은 이미지끼리 블록을 나눠 썼고(질문이 달라도 이미지까지의 앞부분은 적중), 처리기 캐시 지표
vllm:mm_cache_hits_total은 30번 중 20번(두 번째·세 번째 사용) 적중을 셌습니다. - 정답률: 색 10/10, 네 자리 숫자 0/10. 숫자는 6082 를 「6002」, 5435 를 「543」, 9532 를 「932」로 읽는 식으로 한 자리를 바꾸거나 빠뜨렸습니다. 2B 모델의 한계인지 전처리 탓인지는 가르지 못해 미확인입니다.
SSRF: 서버가 대신 내려받는가
| 요청의 이미지 주소 | 기본 서버 | --allowed-media-domains example.com |
|---|---|---|
http://127.0.0.1:8000/health(서버 자신) |
400 「Failed to load image: cannot identify image file」(5.5ms) | 400 「The URL must be from one of the allowed domains」(2.6ms) |
http://127.0.0.1:8000/metrics |
400, 같은 메시지 | 400, 같은 거절 |
file:///etc/passwd |
500 「Cannot load local files without --allowed-local-media-path」 |
같음 |
기본 서버의 메시지는 「이미지가 아니다」입니다. 곧 서버가 그 내부 주소에 실제로 요청을 보내 응답을 받아 왔다는 뜻입니다. 응답 내용이 돌아오지는 않았지만, 요청자는 이 서버를 통해 내부망을 두드리고 응답 시간과 오류로 무엇이 열려 있는지 알아낼 수 있습니다. 도메인을 정하자 받기 전에 거절했습니다. 로컬 파일은 기본으로 막혀 있었습니다. 이미지 URL 을 받는 서비스라면 --allowed-media-domains 는 선택이 아니라 필수입니다.
3. 풀링: 임베딩과 재순위도 같은 엔진으로
생성하지 않고 마지막 은닉 상태를 뽑아 쓰는 모델을 vLLM 은 풀링 모델이라 부릅니다. --runner pooling 으로 띄우면 생성 대신 /v1/embeddings, /score, /rerank, /classify, /pooling 을 제공합니다. 인과 모델은 마지막 토큰(LAST), BERT 계열은 첫 토큰(CLS)을 기본으로 씁니다. 공식 문서는 「풀링 모델 지원은 편의를 위한 것이고 다른 방식보다 빠르다는 보장은 없다」고 먼저 적어 둡니다.
방법
- 임베딩: Qwen3-Embedding-0.6B 를
--runner pooling으로 띄우고, 무작위 단어 120개(약 127토큰)짜리 문장 64개를 한 요청으로 보냈습니다. 여섯 번 중 첫 번은 예열. - 재순위: Qwen3-Reranker-0.6B 는 원래 「yes」와 「no」 토큰의 점수로 관련성을 내는 생성 모델입니다. vLLM 예제의 방법대로
--hf-overrides '{"architectures":["Qwen3ForSequenceClassification"],"classifier_from_token":["no","yes"],"is_original_qwen3_reranker":true}'와 예제의 채팅 템플릿으로 분류 모델처럼 띄웠습니다. 질문 하나와 짧은 문서 32개(6종을 되풀이)를/rerank로 보냈습니다.
결과
| 작업 | 한 요청 | 지연 중앙(5회) | 처리량 | 비고 |
|---|---|---|---|---|
| 임베딩 | 문장 64개 × 약 127토큰 | 371.8ms | 초당 172개(약 21,900토큰) | 차원 1,024 |
| 재순위 | 질문 1 + 문서 32개 | 33.9ms | 초당 944개 | 맞는 문서가 0.993 으로 1위 |
- 임베딩의 토큰 처리 속도(초당 약 21,900토큰)는 같은 GPU 에서 생성 모델의 프리필 속도(2편, 초당 약 18,000토큰)와 비슷했습니다. 풀링은 프리필만 하는 일이기 때문입니다.
- 재순위는 「서빙 엔진이 공통 접두사를 어떻게 다시 쓰는가」라는 질문에 접두사 캐시 문장을 0.993 으로 가장 위에 올렸습니다. 재순위용 학습이 된 모델이라 이 일은 잘합니다.
- 풀링 서버도 KV 캐시를 4.3GiB(40,272토큰) 잡았습니다. 인과 어텐션 풀링에서는 chunked prefill 과 접두사 캐시가 동작한다고 기능 표에 적혀 있어, 그 자리로 쓰입니다. 임베딩만 하는 서버라면
--gpu-memory-utilization을 낮춰 GPU 를 다른 일과 나눌 수 있습니다(미실측). - 보안 문서에 따르면
/score,/rerank,/pooling,/classify처럼/v1이 붙지 않은 경로는--api-key를 줘도 인증을 받지 않습니다. 8편에서 같은 성질의 경로를 실제로 두드려 봤습니다.
4. LoRA 다중 서빙
기본 모델 하나에 LoRA 어댑터 여러 개를 얹어 요청마다 다른 어댑터를 쓰는 기능입니다(--enable-lora --max-loras --max-lora-rank). 같은 8GB급 GPU 에서 1.7B 모델로 잰 값은 LoRA 어댑터 서빙에 있습니다. 어댑터를 쓰면 8.5~17.9% 느렸고, 어댑터 수가 슬롯 수를 넘으면 처리량이 크게 떨어졌습니다. 이번 판에서 덧붙일 것은 두 가지입니다.
- 실행 중 어댑터 적재는 위험한 기능입니다.
VLLM_ALLOW_RUNTIME_LORA_UPDATING=True로 켜는/v1/load_lora_adapter에 대해 문서는 「격리되고 완전히 신뢰하는 환경이 아니면 운영에서 쓰지 말라」고 경고합니다. 요청자가 서버가 읽을 경로를 정하게 되기 때문입니다. - 추측 디코딩과 함께 쓰지 못합니다. 0.32.0 문서의 기능 호환 표에서 LoRA 와 추측 디코딩의 칸은 ❌ 입니다(3편).
5. 확인하지 못한 것
- TP·PP·DP·EP 의 실제 이득과 통신 비용은 GPU 가 한 장이라 미실측입니다.
- 영상·오디오 입력, 여러 장의 이미지, 동시 요청에서의 인코더 지연은 미실측입니다. 숫자를 틀린 원인도 미확인입니다.
- 임베딩 처리량의 배치 크기별 변화, 긴 문서의 재순위, 정확도 평가는 미실측입니다.
참고
- vLLM 문서, Parallelism and Scaling, Data Parallel Deployment, Multimodal Inputs, Conserving Memory, Pooling Models, Security(0.32.0): https://docs.vllm.ai/en/latest/
- Shoeybi 외, 「Megatron-LM: Training Multi-Billion Parameter Language Models Using Model Parallelism」, 2019. https://arxiv.org/abs/1909.08053
- vLLM 예제, Qwen3 재순위 모델을 분류 모델로 띄우기: https://github.com/vllm-project/vllm/blob/v0.31.0/examples/pooling/score/qwen3_reranker_online.py
- vLLM 0.32.0 릴리스 노트(이미지 형식 허용 목록): https://github.com/vllm-project/vllm/releases/tag/v0.32.0
- 이전 글: LoRA 어댑터 서빙, NVIDIA Dynamo