컨테이너를 쓸 수 없는 RHEL 8.6 폐쇄망 GPU 서버(H100, 드라이버 580.x/CUDA 13.0)에 vLLM 0.21 을 pip 휠 번들로 반입해 LLM(Qwen3.6-27B) · 임베딩(bge-m3) · 리랭커(bge-reranker-v2-m3) 세 프로세스를 systemd 로 띄운 기록이다. 사내 A100 서버에서 검증한 venv 를 그대로 고정해 옮기고, 실제 현장에서 만난 컴파일·캐시 문제와 해결책을 담았다. 앞단 게이트웨이는 LiteLLM 게이트웨이에 둔다.
python3.10 -m venv venv && source venv/bin/activate
pip install --upgrade pip setuptools wheel
pip install "vllm==0.21.0" ninja # ninja 는 JIT 컴파일에 필요
mkdir -p /data/vllm-offline/{wheels,models}
pip freeze > /data/vllm-offline/requirements.lock
pip download --only-binary=:all: -r /data/vllm-offline/requirements.lock -d /data/vllm-offline/wheels
pip download --only-binary=:all: pip setuptools wheel -d /data/vllm-offline/wheels
cd /data/vllm-offline/wheels && sha256sum *.whl > ../SHA256SUMS
ls *.whl | wc -l ; wc -l ../requirements.lock # 개수가 비슷해야 정상
hf download Qwen/Qwen3.6-27B --local-dir /data/vllm-offline/models/Qwen3.6-27B
휠 호환성은 cp310 ABI 태그로 결정되므로 사내 3.10.x 와 고객 3.10.12 는 같은 번들을 쓴다. --only-binary=:all: 을 빼면 소스 tar.gz 로 폴백해 현장에서 컴파일하다 실패한다. 고객 서버에서는 ldd --version(glibc 2.28), python3.10 -m venv --help, pip -V(20.3 미만이면 manylinux 태그 인식 실패) 를 미리 확인한다.
sha256sum -c SHA256SUMS
python3.10 -m venv /opt/vllm/venv && source /opt/vllm/venv/bin/activate
pip install --no-index --find-links=./wheels --upgrade pip setuptools wheel
pip install --no-index --find-links=./wheels --only-binary=:all: -r requirements.lock
python -c "import vllm, torch; print(vllm.__version__, torch.cuda.is_available())"
소스 빌드 Python 에서 No module named _lzma 가 나면 xz-devel bzip2-devel libffi-devel openssl-devel sqlite-devel readline-devel 을 설치하고 ./configure && make && make altinstall 로 재빌드한 뒤 venv 를 다시 만든다(make install 은 시스템 python3 을 덮어쓴다).
# /etc/sysconfig/vllm-common — 세 서비스 공통
VLLM_CACHE_ROOT=/var/cache/vllm
TRITON_CACHE_DIR=/var/cache/vllm/triton
XDG_CACHE_HOME=/var/cache/vllm
HF_HOME=/var/cache/vllm/hf
HF_HUB_OFFLINE=1
TRANSFORMERS_OFFLINE=1
VLLM_NO_USAGE_STATS=1
DO_NOT_TRACK=1
VLLM_USE_FLASHINFER_SAMPLER=0 # nvcc 없는 환경에서 FlashInfer JIT 회피
no_proxy=*
NO_PROXY=*
# LLM (8000) — Qwen3 계열 reasoning 끄기
vllm serve /data/models/Qwen3.6-27B --served-model-name Qwen3.6-27B \
--host 127.0.0.1 --port 8000 --max-model-len 32768 --gpu-memory-utilization 0.85 \
--reasoning-parser qwen3 --default-chat-template-kwargs '{"enable_thinking": false}' \
--enable-auto-tool-choice --tool-call-parser qwen3_coder
# 임베딩 (8001)
vllm serve /data/models/bge-m3 --served-model-name bge-m3 --runner pooling \
--host 127.0.0.1 --port 8001 --max-model-len 8192 --gpu-memory-utilization 0.06
# 리랭커 (8002) — cross-encoder 는 아키텍처(...ForSequenceClassification)로 자동 인식
vllm serve /data/models/bge-reranker-v2-m3 --served-model-name bge-reranker-v2-m3 --runner pooling \
--host 127.0.0.1 --port 8002 --max-model-len 8192 --gpu-memory-utilization 0.06
한 GPU 에 셋을 올릴 때 --gpu-memory-utilization 합이 1 을 넘지 않게 나눈다(기본 0.9 로 두 번째를 띄우면 즉시 OOM). GPU 가 여럿이면 CUDA_VISIBLE_DEVICES 로 분리한다. 요청 단위 chat_template_kwargs 는 서버 기본값보다 우선하므로 thinking 을 요청별로 다시 켤 수 있다.
curl -s http://127.0.0.1:8001/v1/embeddings -H 'Content-Type: application/json' \
-d '{"model":"bge-m3","input":["첫 문장","둘째 문장"],"encoding_format":"float"}'
curl -s http://127.0.0.1:8002/v1/rerank -H 'Content-Type: application/json' \
-d '{"model":"bge-reranker-v2-m3","query":"RHEL 8 기본 커널은?","documents":["RHEL 8 은 4.18 계열 커널","오늘 메뉴"],"top_n":1}'
# /etc/systemd/system/vllm-llm.service
[Unit]
Description=vLLM LLM (Qwen3.6-27B)
After=network-online.target
Wants=network-online.target
StartLimitIntervalSec=600
StartLimitBurst=3
[Service]
Type=simple
User=vllm
Group=vllm
WorkingDirectory=/opt/vllm
EnvironmentFile=-/etc/sysconfig/vllm-common
EnvironmentFile=-/etc/sysconfig/vllm-llm
Environment=PATH=/opt/vllm/venv/bin:/usr/local/bin:/usr/bin:/bin
ExecStart=/opt/vllm/venv/bin/vllm serve /data/models/Qwen3.6-27B --served-model-name Qwen3.6-27B --host 127.0.0.1 --port 8000 --max-model-len 32768 --gpu-memory-utilization 0.85
Restart=on-failure
RestartSec=15
TimeoutStopSec=60
LimitNOFILE=65536
ProtectHome=true
ReadWritePaths=/var/cache/vllm
[Install]
WantedBy=multi-user.target
ExecStart 는 절대경로여야 하고(status 203/EXEC 는 경로·실행권한 문제), 셸 스크립트로 감쌀 때는 스크립트 안에서 venv 를 activate 하거나 절대경로 바이너리를 부른다. systemd 는 PATH 를 거의 물려주지 않으므로 Environment=PATH=... 로 venv 의 bin 을 앞에 둔다. ProtectHome=true 와 홈 밑 캐시가 충돌하니 캐시를 /var/cache/vllm 으로 뺀다. 로그는 journalctl -u vllm-llm -f.
| 증상 | 원인 | 조치 |
|---|---|---|
could not find nvcc ... CUDA_HOME=/usr/local/cuda doesn't exist |
FlashInfer 샘플러가 JIT 컴파일을 시도 | VLLM_USE_FLASHINFER_SAMPLER=0 이 프로세스 환경에 실제로 있는지 tr '\0' '\n' < /proc/<pid>/environ 로 확인 |
GDN prefill 커널 컴파일 실패, ptxas 없음 |
torch 휠이 ptxas 를 들고 오지만 경로를 모름 | export TRITON_PTXAS_PATH=$(find /opt/vllm/venv -name ptxas \| head -1); export CUDA_HOME=$(dirname $(dirname $TRITON_PTXAS_PATH)) |
FileNotFoundError: 'ninja' → 뒤이어 LiteLLM Cannot connect to host 127.0.0.1:8000 |
Triton JIT 가 ninja 빌드 도구를 못 찾음 | pip install ninja 후 PATH 에 venv/bin 추가. 폐쇄망이면 ninja·cmake 휠 반입 |
| 사내에선 되고 현장에선 첫 요청에서 죽음 | torch_compile_cache 키에 모델 경로가 포함돼 경로가 바뀌면 재컴파일 |
ninja 를 갖춰 언제든 컴파일되게 두는 것이 정상 상태. 캐시는 가속일 뿐 |
FlashInfer exit code=127 |
컴파일러 바이너리 경로/공유 라이브러리 누락 | $TRITON_PTXAS_PATH --version, ldd 확인. 안 되면 pip uninstall flashinfer-python flashinfer-cubin flashinfer-jit-cache |
| Qwen3.6 GDN 커널 컴파일 우회 | FlashInfer 경로 대신 Triton 경로 강제 | --compilation-config '{"custom_ops": ["-chunk_gated_delta_rule"]}' --gpu-memory-utilization 0.82 (--gdn-prefill-backend 옵션은 존재하지 않음) |
System message must be at the beginning (Agent Studio 등 클라이언트가 system 을 뒤에 보냄) |
Qwen3 chat template 의 검증 | 모델의 chat_template.jinja 를 복사해 해당 raise_exception 만 완화한 템플릿을 --chat-template 로 지정 |
HF_HUB_OFFLINE=1 등이 없으면 외부 접속을 시도하다 타임아웃으로 멈춘다.