openai, google이 제공하는 API 호출해서 써보기만 했던 LLM을, 폐쇄망 환경에서 VLLM과 llama.cpp을 각각 사용하여 직접 로드하고 호스팅해 보았다.
폐쇄망에서는 단순히 LLM의 이름만으로 huggingface를 활용하여 모델을 불러오고, 관련 파이썬 패키지를 다운받는 것이 불가능하다. 그래서 아래의 준비물들이 필요하다.
따라서 오프라인에서 LLM을 로드하기 위해서는 LLM 폴더 자체 혹은 LLM gguf 파일이 필요하다. LLM 폴더는 huggingface 아이디를 활용하여 git clone으로 받을 수 있고, gguf 파일도 마찬가지로 huggingface web 상에서 다운로드 받을 수 있다.
모델 폴더 다운로드 예시
git clone https://huggingface.co/openai-community/gpt2 # root 폴더에 gpt2 모델 폴더 생성됨
파이썬 패키지도 폐쇄망이기 때문에 미리 다운로드를 받아야한다. pip download 를 활용하여 whl 파일로 저장해두고, LLM 로드 전 서버에 설치한다.
주의할 점은 아래의 패키지를 다운로드할 때 pip이 폐쇄망 내에서 사용할 파이썬 환경과 일치해야한다는 점이다.
VLLM 활용 파이썬 패키지 다운로드 예시
mkdir python-packages # 다운로드 받을 폴더 생성
pip download vllm -d ./python-packages # vllm 의존성 패키지를 생성한 폴더에 다운로드. 휠파일 생성
llama.cpp 활용 파이썬 패키지 다운로드 예시
mkdir python-packages
# vllm과 다르게 별도로 설치해줘야하는 의존성들이 있다
pip download \
"llama-cpp-python[server]" \
scikit-build-core \
cmake \
ninja \
pydantic-core \
setuptools \
wheel \
-d ./python-packages
그리고 파이썬 패키지를 다운로드하고, python -m vllm..으로 시작하는 파이썬을 동작시킬 command line을 저장시킨 실행 스크립트가 있으면 좋다.
VLLM은 GPU VRAM의 일정 부분을 할당하여 LLM을 로드하고, PagedAttention 알고리즘을 활용하여 메모리를 효율적으로 관리한다. 이를 통해 배치 처리를 최적화하고 더 많은 동시 요청을 처리할 수 있다.
실행 스크립트는 다음의 항목을 포함해야한다.
1. 폐쇄망 환경 설정
2. 파이썬 패키지 설치
3. python -m vllm.entrypoints.openai.api_server 실행
아래의 항목을 스크립트에 입력함으로써 huggingface가 인터넷에 접속하려는 시도를 막는다.
그리고 기타 환경변수를 설정해준다.
# start.sh
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export MODEL_PATH="${SCRIPT_DIR}/gemma-3-270m-it"
export HF_DATASETS_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export HF_HUB_OFFLINE=1
export HF_DATASETS_CACHE="${SCRIPT_DIR}/.cache/huggingface/datasets"
export HF_HOME="${SCRIPT_DIR}/.cache/huggingface"
# ...
아래의 항목을 스크립트에 입력함으로써 vllm 구동을 위한 패키지를 폐쇄망에 설치한다.
# start.sh
# ...
pip install --no-index --find-links "${SCRIPT_DIR}/python-packages" \
vllm
# ...
아래의 항목을 스크립트 마지막에 입력함으로써 llm로드와 호스팅을 진행한다.
# start.sh
# ...
python -m vllm.entrypoints.openai.api_server \
--host 0.0.0.0 \
--port 8000 \
--model "${MODEL_PATH}" \
--gpu-memory-utilization 0.75 \
--max-model-len 4096 \
--trust-remote-code
llama.cpp는 GPU 없이 CPU만으로도 LLM을 실행할 수 있도록 최적화된 라이브러리이다. GGUF 포맷의 양자화된 모델을 사용하여 메모리 사용량을 줄이고 CPU에서도 합리적인 속도로 추론을 수행할 수 있다.
실행 스크립트는 다음의 항목을 포함해야한다.
1. 폐쇄망 환경 설정
2. 파이썬 패키지 설치
3. python -m llama_cpp.server 실행
아래의 항목을 스크립트에 입력함으로써 모델 경로와 기타 환경변수를 설정해준다.
# start.sh
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
export MODEL_PATH="${SCRIPT_DIR}/gemma-3-1b-it-q4_0.gguf"
# ...
아래의 항목을 스크립트에 입력함으로써 llama-cpp-python 구동을 위한 패키지를 폐쇄망에 설치한다.
# start.sh
# ...
pip install --no-index --find-links "${SCRIPT_DIR}/python-packages" \
"llama-cpp-python[server]"
# ...
아래의 항목을 스크립트 마지막에 입력함으로써 llm로드와 호스팅을 진행한다.
# start.sh
# ...
python -m llama_cpp.server \
--model "${MODEL_PATH}" \
--host 0.0.0.0 \
--port 8001 \
--n_threads 4
주요 파라미터 설명:
--n_threads: CPU 스레드 개수 (CPU 코어 수에 맞춰 조정)간단하게 curl로 테스트해볼 수 있다.
VLLM 서버 테스트 (포트 8000)
# Health check
curl http://localhost:8000/health
# 모델 목록 조회
curl http://localhost:8000/v1/models
# Chat completion 테스트
curl -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "gemma-3-270m-it",
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 100
}'
llama.cpp 서버 테스트 (포트 8001)
# Health check
curl http://localhost:8001/health
# Chat completion 테스트
curl -X POST http://localhost:8001/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"messages": [{"role": "user", "content": "Hello!"}],
"max_tokens": 100
}'
{{< anchor id="test-ref" >}}혹은 테스트를 위한 파이썬 파일을 생성하여 아래와 같이 테스트해볼 수 있다.
# VLLM 서버 테스트
python llm_test.py localhost 8000
# llama.cpp 서버 테스트
python llm_test.py localhost 8001
uv가 설치되어있다는 전제로 uv를 활용해서 의존성 패키지를 설치했다.
#!/bin/bash
# Exit on error
set -e
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "========================================="
echo "LLM Load & Host - vLLM (Closed Network)"
echo "========================================="
echo "Script Directory: ${SCRIPT_DIR}"
echo "========================================="
echo ""
# Step 1: Configure environment for offline mode
echo "[1/5] Configuring offline environment..."
export HF_DATASETS_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export HF_HUB_OFFLINE=1
export HF_DATASETS_CACHE="${SCRIPT_DIR}/.cache/huggingface/datasets"
export HF_HOME="${SCRIPT_DIR}/.cache/huggingface"
# Configure UV for offline mode
export UV_NO_CACHE=1
export UV_OFFLINE=1
export UV_FIND_LINKS="${SCRIPT_DIR}/python-packages"
# Point to local model directory
export MODEL_PATH="${SCRIPT_DIR}/gemma-3-270m-it"
echo "✓ Environment configured for offline mode"
echo ""
# Step 2: Check if required directories exist
echo "[2/5] Checking required directories..."
if [ ! -d "${MODEL_PATH}" ]; then
echo "ERROR: Model directory not found at ${MODEL_PATH}"
exit 1
fi
echo "✓ Model directory found: ${MODEL_PATH}"
if [ ! -d "${SCRIPT_DIR}/python-packages" ]; then
echo "ERROR: Python packages directory not found at ${SCRIPT_DIR}/python-packages"
exit 1
fi
echo "✓ Python packages directory found"
echo ""
# Step 3: Check if UV is installed
echo "[3/5] Checking UV installation..."
if ! command -v uv &> /dev/null; then
echo "ERROR: UV is not installed. Please install UV first."
exit 1
fi
echo "✓ UV is installed: $(uv --version)"
echo ""
# Step 4: Initialize UV project and install dependencies
echo "[4/5] Setting up Python environment with UV..."
# Check if .venv already exists
if [ -d "${SCRIPT_DIR}/.venv" ]; then
echo "Virtual environment already exists, skipping creation..."
else
echo "Creating virtual environment..."
cd "${SCRIPT_DIR}"
uv venv
fi
# Install dependencies from local packages (including vLLM)
echo "Installing dependencies from local packages..."
cd "${SCRIPT_DIR}"
# Install packages using uv with local packages directory
# Note: vLLM doesn't need accelerate, only torch, transformers, vllm and dependencies
uv pip install --no-index --find-links "${SCRIPT_DIR}/python-packages" \
vllm
echo "✓ Dependencies installed from local packages"
echo ""
# Step 5: Start the vLLM server
echo "[5/5] Starting vLLM API server..."
echo "========================================="
echo "Server Configuration:"
echo " - Model: ${MODEL_PATH}"
echo " - Host: 0.0.0.0"
echo " - Port: 8000"
echo " - GPU Memory Utilization: 0.75"
echo " - Max Model Length: 4096"
echo "========================================="
echo "Server will be accessible at:"
echo " - Local: http://127.0.0.1:8000"
echo " - Network: http://$(hostname -I | awk '{print $1}'):8000"
echo " - API docs: http://$(hostname -I | awk '{print $1}'):8000/docs"
echo " - OpenAI-compatible: http://$(hostname -I | awk '{print $1}'):8000/v1"
echo "========================================="
echo ""
# Run the vLLM server using UV
cd "${SCRIPT_DIR}"
uv run python -m vllm.entrypoints.openai.api_server \
--host 0.0.0.0 \
--port 8000 \
--model "${MODEL_PATH}" \
--gpu-memory-utilization 0.75 \
--max-model-len 4096 \
--trust-remote-code
uv가 설치되어있다는 전제로 uv를 활용해서 의존성 패키지를 설치했다.
#!/bin/bash
# Exit on error
set -e
# Get the directory where this script is located
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
echo "========================================="
echo "LLM Load & Host - llama.cpp (Closed Network)"
echo "========================================="
echo "Script Directory: ${SCRIPT_DIR}"
echo "========================================="
echo ""
# Step 1: Configure environment for offline mode
echo "[1/5] Configuring offline environment..."
# Configure UV for offline mode
export UV_NO_CACHE=1
export UV_OFFLINE=1
export UV_FIND_LINKS="${SCRIPT_DIR}/python-packages"
# Point to local model file
export MODEL_PATH="${SCRIPT_DIR}/gemma-3-1b-it-q4_0.gguf"
echo "✓ Environment configured for offline mode"
echo ""
# Step 2: Check if required files exist
echo "[2/5] Checking required files..."
if [ ! -f "${MODEL_PATH}" ]; then
echo "ERROR: Model file not found at ${MODEL_PATH}"
exit 1
fi
echo "✓ Model file found: ${MODEL_PATH}"
if [ ! -d "${SCRIPT_DIR}/python-packages" ]; then
echo "ERROR: Python packages directory not found at ${SCRIPT_DIR}/python-packages"
exit 1
fi
echo "✓ Python packages directory found"
echo ""
# Step 3: Check if UV is installed
echo "[3/5] Checking UV installation..."
if ! command -v uv &> /dev/null; then
echo "ERROR: UV is not installed. Please install UV first."
exit 1
fi
echo "✓ UV is installed: $(uv --version)"
echo ""
# Step 4: Initialize UV project and install dependencies
echo "[4/5] Setting up Python environment with UV..."
# Check if .venv already exists
if [ -d "${SCRIPT_DIR}/.venv" ]; then
echo "Virtual environment already exists, skipping creation..."
else
echo "Creating virtual environment..."
cd "${SCRIPT_DIR}"
uv venv
fi
# Install dependencies from local packages
echo "Installing dependencies from local packages..."
cd "${SCRIPT_DIR}"
# Install llama-cpp-python with server support
uv pip install --no-index --find-links "${SCRIPT_DIR}/python-packages" \
"llama-cpp-python[server]"
echo "✓ Dependencies installed from local packages"
echo ""
# Step 5: Start the llama.cpp server
echo "[5/5] Starting llama.cpp API server..."
echo "========================================="
echo "Server Configuration:"
echo " - Model: ${MODEL_PATH}"
echo " - Host: 0.0.0.0"
echo " - Port: 8001"
echo " - Threads: 4"
echo "========================================="
echo "Server will be accessible at:"
echo " - Local: http://127.0.0.1:8001"
echo " - Network: http://$(hostname -I | awk '{print $1}'):8001"
echo " - API docs: http://$(hostname -I | awk '{print $1}'):8001/docs"
echo " - OpenAI-compatible: http://$(hostname -I | awk '{print $1}'):8001/v1"
echo "========================================="
echo ""
# Run the llama.cpp server using UV
cd "${SCRIPT_DIR}"
uv run python -m llama_cpp.server \
--model "${MODEL_PATH}" \
--host 0.0.0.0 \
--port 8001 \
--n_threads 4
#!/usr/bin/env python3
"""
Test client for vLLM OpenAI-compatible API
Usage: python test_vllm_client.py <server_ip> [port]
Example: python test_vllm_client.py 192.168.1.100 8000
"""
import sys
import requests
import json
def test_vllm_api(server_ip: str, port: int = 8000):
"""Test the vLLM OpenAI-compatible API endpoints."""
base_url = f"http://{server_ip}:{port}"
api_url = f"{base_url}/v1"
print("=" * 60)
print(f"Testing vLLM API at {base_url}")
print("=" * 60)
print()
# Test 1: Health check
print("[1/4] Testing health endpoint...")
try:
response = requests.get(f"{base_url}/health", timeout=5)
response.raise_for_status()
print("✓ Health check passed")
try:
# Try to parse as JSON
health_data = response.json()
print(json.dumps(health_data, indent=2))
except:
# If not JSON, just print the text
print(f"Response: {response.text}")
except Exception as e:
print(f"✗ Health check failed: {e}")
return False
print()
# Test 2: List models
print("[2/4] Testing models endpoint...")
try:
response = requests.get(f"{api_url}/models", timeout=5)
response.raise_for_status()
print("✓ Models endpoint working")
models = response.json()
print(json.dumps(models, indent=2))
# Get the model name for later use
if models.get("data"):
model_name = models["data"][0]["id"]
print(f"\nUsing model: {model_name}")
else:
print("Warning: No models found")
model_name = None
except Exception as e:
print(f"✗ Models endpoint failed: {e}")
return False
print()
# Test 3: Chat completions (OpenAI-compatible)
print("[3/4] Testing chat completions...")
try:
test_messages = [{"role": "user", "content": "Hello! How are you today?"}]
print(f"Messages: {json.dumps(test_messages, indent=2)}")
print("Generating... (this may take a moment)")
response = requests.post(
f"{api_url}/chat/completions",
json={
"model": model_name or "gemma-3-270m-it",
"messages": test_messages,
"max_tokens": 100,
"temperature": 0.7,
},
timeout=60,
)
response.raise_for_status()
result = response.json()
print("✓ Chat completion successful")
print()
print("-" * 60)
print("Request:")
print(json.dumps(test_messages, indent=2))
print("-" * 60)
print("Response:")
if "choices" in result and len(result["choices"]) > 0:
message = result["choices"][0]["message"]["content"]
print(message)
print("-" * 60)
print("Usage:", result.get("usage", {}))
else:
print(json.dumps(result, indent=2))
print("-" * 60)
except Exception as e:
print(f"✗ Chat completion failed: {e}")
return False
print()
# Test 4: Text completions (OpenAI-compatible)
print("[4/4] Testing text completions...")
try:
test_prompt = "Once upon a time"
print(f"Prompt: {test_prompt}")
print("Generating... (this may take a moment)")
response = requests.post(
f"{api_url}/completions",
json={
"model": model_name or "gemma-3-270m-it",
"prompt": test_prompt,
"max_tokens": 50,
"temperature": 0.7,
},
timeout=60,
)
response.raise_for_status()
result = response.json()
print("✓ Text completion successful")
print()
print("-" * 60)
print("Prompt:", test_prompt)
print("-" * 60)
print("Completion:")
if "choices" in result and len(result["choices"]) > 0:
text = result["choices"][0]["text"]
print(text)
print("-" * 60)
print("Usage:", result.get("usage", {}))
else:
print(json.dumps(result, indent=2))
print("-" * 60)
except Exception as e:
print(f"✗ Text completion failed: {e}")
return False
print()
print("=" * 60)
print("✓ All tests passed!")
print("=" * 60)
print()
print("vLLM API is ready to use!")
print()
print("OpenAI-compatible endpoints:")
print(f" - Chat: POST {api_url}/chat/completions")
print(f" - Completions: POST {api_url}/completions")
print(f" - Models: GET {api_url}/models")
print()
print("Interactive docs at:")
print(f" {base_url}/docs")
print()
return True
def main():
"""Main function."""
if len(sys.argv) < 2:
print("Usage: python test_vllm_client.py <server_ip> [port]")
print("Example: python test_vllm_client.py 192.168.1.100 8000")
sys.exit(1)
server_ip = sys.argv[1]
port = int(sys.argv[2]) if len(sys.argv) > 2 else 8000
try:
success = test_vllm_api(server_ip, port)
sys.exit(0 if success else 1)
except KeyboardInterrupt:
print("\nTest interrupted by user")
sys.exit(1)
if __name__ == "__main__":
main()