Post

(폐쇄망 LLM 7-3) vLLM 서버 설치 가이드

내부망/폐쇄망에서 vLLM으로 로컬 LLM 서버를 구축하는 실전 가이드. Docker 이미지와 HuggingFace 모델을 오프라인 환경으로 전송하고 AI 서비스를 구동합니다.

(폐쇄망 LLM 7-3) vLLM 서버 설치 가이드

이전 글: Docker 환경 구축

이 글은 망분리 환경 AI 배포 시리즈의 아홉 번째 글입니다.

다음 글: Open-WebUI 설치

이번 글의 목표

이제 본격적으로 vLLM 서버를 구축합니다.

전체 흐름은 다음과 같습니다:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
┌──────────────────────────────────────────────────────────────────┐
│                        Internet PC                               │
├──────────────────────────────────────────────────────────────────┤
│  1. Docker pull vLLM image                                       │
│  2. Docker save -> vllm.tar                                      │
│  3. huggingface-cli download model                               │
└──────────────────────────────────────────────────────────────────┘
                              │
                              ▼
                    [ Data Transfer System ]
                              │
                              ▼
┌──────────────────────────────────────────────────────────────────┐
│                        Air-gapped PC                             │
├──────────────────────────────────────────────────────────────────┤
│  4. Docker load <- vllm.tar                                      │
│  5. docker run vllm                                              │
│  6. Health check & test                                          │
└──────────────────────────────────────────────────────────────────┘

1. vLLM Docker 이미지 설치

vLLM 공식 이미지를 받습니다. 태그는 Docker Hub에서 확인할 수 있습니다. 2-3일 주기로 이미지가 업데이트 됩니다. 최신 업데이트를 보면 두가지가 보이는데요 nightlylatest 버전이 있습니다. 다음과 같이 해석하면 됩니다:

nightly가 안정성은 떨어지지만 업데이트가 빠릅니다. 요즘 HuggingFace에 신규 모델들이 쏟아지고 있고, vLLM 커밋 히스토리를 보면 하루에도 몇 개씩 버그 수정과 하드웨어 지원이 추가되고 있습니다. 개인적으로 nightly 버전을 선호합니다. 자, 이걸 받아서 옮겨볼까요? 전체 흐름은 다음과 같습니다:

  • docker pull: vLLM 이미지 다운로드 (의존성 포함)
  • docker save: 이미지를 tar 파일로 저장
  • 압축 (필요시): gzip으로 용량 절반으로 줄이기
  • 망간 이동: GPU 머신으로 파일 전송.
  • docker load: tar 파일에서 이미지 복원

먼저 인터넷 환경에서 다음을 진행합니다:

1
2
3
4
# 인터넷망
docker pull vllm/vllm-openai:nightly  # Docker Hub에서 이미지 다운로드
docker save -o vllm-openai.tar vllm/vllm-openai:nightly  # -o: output 파일 경로 지정
gzip vllm-openai.tar  # 압축 후 원본 .tar 삭제되고 .tar.gz 생성

이제 gz 파일을 업무망 (혹은 전산실, IDC같은 중요망)에 옮깁니다. 보안 USB 같은 매체를 허용하는 회사라면 조금 더 편하게 진행할 수 있고, 아니라면 압축 파일을 허용 범위로 쪼개서 망간 자료 전송을 해야 합니다. 옮기는 작업이 완료 됐다면

1
2
3
4
# 업무망 중요망등 폐쇄망
gunzip vllm-openai.tar.gz  # 압축 해제, .tar.gz -> .tar (원본 .gz 삭제)
docker load -i vllm-openai.tar  # -i: input 파일에서 이미지 복원
docker images | grep vllm  # 이미지 복원 확인

nightly 버전이 잘 설치됐다면 다음과 같이 출력됩니다:

1
2
IMAGE                                 ID                 DISK USAGE     CONTENT SIZE
vllm/vllm-openai:nightly              31e08c7f6d05       28.8GB         8.98GB

2. 모델 다운로드

모델은 huggingface-cli 를 이용해서 HuggingFace에서 다운로드하면 됩니다. 이 파일을 옮기면 끝입니다.

1
2
3
4
5
6
7
8
# 인터넷 망에서
pip install huggingface_hub  # huggingface-cli 설치

# 모델 다운로드
# - mistralai/Devstral-Small-2-24B-Instruct-2512: HuggingFace 모델 경로
# - --local-dir: 로컬 저장 디렉토리
hf download mistralai/Devstral-Small-2-24B-Instruct-2512 \
  --local-dir ./Devstral-Small-2-24B-Instruct-2512-FP8

현재 경로에 Devstral-Small-2-24B-Instruct-2512-FP8 폴더가 생성되고 모델 파일이 저장됩니다. 다운로드 중 네트워크 오류 등으로 중단되면 같은 명령어를 다시 실행하세요. 이미 받은 파일은 건너뛰고 이어서 다운로드됩니다.

다운로드가 완료되면 Devstral-Small-2-24B-Instruct-2512-FP8 폴더 전체를 망간 자료 전송으로 GPU 서버에 옮깁니다. 어느 경로든 상관없지만, 이 글에서는 ~/local-models에 옮겼다고 가정하고 진행하겠습니다.

3. vLLM 서버 실행

GPU가 있는 폐쇄망(업무망 혹은 중요망)에 모델과 Docker, 그리고 vLLM 이미지가 잘 옮겨졌다면 이제 vLLM으로 모델을 올려봅시다. 다음은 스크립트 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#!/bin/bash

MODEL="Devstral-Small-2-24B-Instruct-2512-FP8"
PORT=8080
VLLM_PORT=8000

docker run -d --rm --name vllm-server \
    --gpus all \
    -p ${PORT}:${VLLM_PORT} \
    -v ~/local-models:/models \
    -v ~/.cache/vllm:/root/.cache/vllm \
    -v ~/.cache/flashinfer:/root/.cache/flashinfer \
    -v ~/.cache/torch:/root/.cache/torch \
    -v ~/.triton:/root/.triton \
    -e NCCL_P2P_DISABLE=1 \
    -e VLLM_USE_FLASHINFER_SAMPLER=1 \
    -e NCCL_DEBUG=INFO \
    -e VLLM_USE_DEEP_GEMM=0 \
    vllm/vllm-openai:nightly \
    --model /models/${MODEL} \
    --disable-custom-all-reduce \
    --attention-backend FLASHINFER \
    --max-model-len 150000 \
    --tensor-parallel-size 2 \
    --max-num-seqs 6 \
    --swap-space 64 \
    --enable-chunked-prefill \
    --enable-prefix-caching \
    --max-num-batched-tokens 16384 \
    --gpu-memory-utilization 0.93 \
    --tool-call-parser mistral \
    --enable-auto-tool-choice \
    --host 0.0.0.0 \
    --port ${VLLM_PORT} \
    --dtype auto \
    --tokenizer-mode auto \
    --trust-remote-code \
    --served-model-name devstral-small-2

아래는 옵션에 대한 설명입니다. 아직 하드웨어 미지원 상태라 붙는 옵션이 많습니다. nightly 버전을 받는 이유이기도 하고, vLLM을 자주 업데이트해야 하는 이유이기도 합니다.

Docker 옵션

  • -d: detach 모드로 백그라운드 실행
  • --rm: 컨테이너 종료 시 자동 삭제
  • --name vllm-server: 컨테이너 이름 지정
  • --gpus all: 모든 GPU 할당
  • -p ${PORT}:${VLLM_PORT}: 호스트 포트와 컨테이너 포트 매핑

볼륨 마운트 (-v)

  • ~/local-models:/models: 다운로드한 모델 폴더를 컨테이너에 마운트
  • ~/.cache/vllm:/root/.cache/vllm: vLLM 컴파일 캐시. 컨테이너 재시작 시 재컴파일 방지
  • ~/.cache/flashinfer:/root/.cache/flashinfer: FlashInfer 커널 캐시
  • ~/.cache/torch:/root/.cache/torch: PyTorch 커널 캐시
  • ~/.triton:/root/.triton: Triton 컴파일 캐시. GPU별 최적화 커널 저장

캐시 마운트를 안 하면 컨테이너 시작할 때마다 커널 컴파일을 다시 합니다. 처음엔 10분 넘게 걸릴 수 있어서 꼭 마운트하세요.

환경 변수 (-e)

  • NCCL_P2P_DISABLE=1: GPU 간 P2P 통신 비활성화. SM120 (RTX Pro 6000 등 Blackwell 계열)에서 컴파일 에러 방지
  • VLLM_USE_FLASHINFER_SAMPLER=1: FlashInfer 샘플러 사용. top-k/top-p 샘플링 최적화. 미설정 시 PyTorch-native 구현으로 fallback
  • NCCL_DEBUG=INFO: NCCL 디버그 로그 출력. 멀티 GPU 통신 문제 디버깅용
  • VLLM_USE_DEEP_GEMM=0: DeepSeek의 행렬 연산 최적화 라이브러리 비활성화. DeepGEMM은 SM90/SM100만 지원하고 SM120에서는 tcgen05.fence 에러 발생

vLLM 서버 옵션

  • --model /models/${MODEL}: 로드할 모델 경로
  • --attention-backend FLASHINFER: FlashInfer 어텐션 백엔드 사용. 기본값 FA3(Flash Attention 3)는 SM120 미지원
  • --disable-custom-all-reduce: vLLM 내장 all-reduce 최적화 비활성화. SM120에서 미지원
  • --tensor-parallel-size 2: 2개 GPU에 모델 분산. GPU 메모리가 부족할 때 늘리면 됨
  • --max-model-len 150000: 최대 컨텍스트 길이. 높을수록 KV Cache 메모리 사용량 증가
  • --max-num-seqs 6: 동시 처리 가능한 시퀀스 수
  • --max-num-batched-tokens 16384: prefill 단계에서 한 번에 처리할 최대 토큰 수. 이전 글에서 설명한 KV Cache 크기와 직결됨
  • --gpu-memory-utilization 0.93: GPU 메모리 사용률. 0.93이면 VRAM의 93%를 vLLM에 할당
  • --swap-space 64: KV Cache 부족 시 CPU 메모리로 스왑할 공간 (GB). preemption 발생 시 KV Cache를 CPU로 내려서 다른 요청 처리 후 다시 올림
  • --enable-chunked-prefill: 긴 프롬프트를 청크 단위로 나눠서 prefill. 짧은 요청의 지연시간 개선
  • --enable-prefix-caching: 동일 prefix를 가진 요청들의 KV Cache 재사용
  • --tool-call-parser mistral: Mistral 모델용 tool call 파서
  • --enable-auto-tool-choice: 모델이 자동으로 tool 사용 여부 결정
  • --host 0.0.0.0: 모든 네트워크 인터페이스에서 접속 허용
  • --port ${VLLM_PORT}: 서버 포트
  • --dtype auto: 모델 데이터 타입 자동 감지
  • --tokenizer-mode auto: 토크나이저 모드 자동 선택
  • --trust-remote-code: 모델의 커스텀 코드 실행 허용. HuggingFace 모델 중 일부는 이 옵션 필수
  • --served-model-name devstral-small-2: API에서 사용할 모델 이름. 클라이언트에서 이 이름으로 호출

현재 -d 로 돌렸기 때문에 백그라운드에서 돌아가고 있습니다. 로그로 프로세스가 잘 올라오고 있나 확인해 봅시다.

1
docker logs -f vllm-server

모델 로딩에 몇 분 걸릴 수 있습니다. 정상 시작 시 마지막에 다음과 같은 메시지가 나옵니다:

1
2
3
4
5
6
(APIServer pid=1) INFO 01-01 15:24:14 [launcher.py:46] Route: /v1/chat/completions, Methods: POST
(APIServer pid=1) INFO 01-01 15:24:14 [launcher.py:46] Route: /v1/completions, Methods: POST
...
(APIServer pid=1) INFO:     Started server process [1]
(APIServer pid=1) INFO:     Waiting for application startup.
(APIServer pid=1) INFO:     Application startup complete.

4. Health Check

서버가 정상 동작하는지 확인합니다:

1
curl http://localhost:8080/health

정상이면 HTTP 200과 빈 응답이 돌아옵니다.

5. 모델 목록 확인

사용 가능한 모델을 확인합니다:

1
curl http://localhost:8080/v1/models

응답 예시:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
{
  "object": "list",
  "data": [
    {
      "id": "devstral-small-2",
      "object": "model",
      "created": 1735948800,
      "owned_by": "vllm",
      "root": "devstral-small-2",
      "parent": null,
      "max_model_len": 150000,
      "permission": [
        {
          "id": "modelperm-abc123",
          "object": "model_permission",
          "created": 1735948800,
          "allow_create_engine": false,
          "allow_sampling": true,
          "allow_logprobs": true,
          "allow_search_indices": false,
          "allow_view": true,
          "allow_fine_tuning": false,
          "organization": "*",
          "group": null,
          "is_blocking": false
        }
      ]
    }
  ]
}

6. 간단한 테스트

실제로 추론이 되는지 테스트합니다:

1
2
3
4
5
6
7
8
9
curl http://localhost:8080/v1/chat/completions \
  -H "Content-Type: application/json" \
  -d '{
    "model": "devstral-small-2",
    "messages": [
      {"role": "user", "content": "Hello, how are you?"}
    ],
    "max_tokens": 100
  }'

응답이 오면 성공입니다.

다음 글

vLLM 서버가 올라갔습니다. 다음 글에서는 Open-WebUI를 설치하고 vLLM 서버와 연동합니다.


시리즈 목차

전체 목차는 AI 활용에서 확인하실 수 있습니다.

This post is licensed under CC BY 4.0 by the author.