#!/usr/bin/env bash
# ==========================================================================
# STAGE 3a: vLLM 추론 서빙 벤치마크 (v3 - Air-gap 대응)
# 신규: HF_HUB_OFFLINE 등 오프라인 강제 + 모델 지정을 전부 로컬 경로로 전환
# (--served-model-name 으로 친숙한 이름 부여, 클라이언트도 그 이름으로 호출)
# ==========================================================================
set -euo pipefail
# --- Air-gap 강제: 이 스크립트 실행 중 어떤 라이브러리도 인터넷(HF Hub 등) 접근 금지 ---
export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export HF_DATASETS_OFFLINE=1
export HF_HOME="${HF_HOME:-/mnt/local-nvme-cache/huggingface}"
# --- 로컬에 미리 받아둔 모델 경로 (실제 반입 경로에 맞게 수정) ---
LOCAL_MODEL_ROOT="${LOCAL_MODEL_ROOT:-/mnt/local-nvme-cache/models}"
MODEL_70B="${MODEL_70B:-${LOCAL_MODEL_ROOT}/Meta-Llama-3.1-70B-Instruct}"
MODEL_8B="${MODEL_8B:-${LOCAL_MODEL_ROOT}/Meta-Llama-3.1-8B-Instruct}"
MODEL_DEEPSEEK="${MODEL_DEEPSEEK:-${LOCAL_MODEL_ROOT}/DeepSeek-R1}"
TP_SIZE=8
GPU_MEM_UTIL=0.90
MAX_MODEL_LEN=65536
VLLM_PORT=8000
SERVER_HOST="127.0.0.1"
LOG_DIR="/var/log/b300_validation/stage3a_vllm_$(date +%Y%m%d_%H%M%S)"
mkdir -p "${LOG_DIR}"
log() { echo -e "\n\033[1;32m[$(date '+%Y-%m-%d %H:%M:%S')] $1\033[0m"; }
source ~/vllm-bench-env/bin/activate
# 사전 점검: 로컬 모델 디렉터리가 실제로 존재하는지 (없으면 여기서 바로 중단 -> HF로 새는 것 방지)
for d in "${MODEL_70B}" "${MODEL_8B}" "${MODEL_DEEPSEEK}"; do
[ -d "${d}" ] || { echo "모델 디렉터리 없음: ${d} (air-gap 반입 여부 확인)"; exit 1; }
done
start_vllm_server() {
local model_path=$1 served_name=$2 quant_flag=$3 prefix_cache=$4 log_tag=$5
local extra=""
[ "${quant_flag}" != "none" ] && extra="--quantization ${quant_flag}"
[ "${prefix_cache}" = "true" ] && extra="${extra} --enable-prefix-caching" || extra="${extra} --no-enable-prefix-caching"
log "vLLM 기동: ${model_path} (served-name=${served_name}, quant=${quant_flag}, prefix_cache=${prefix_cache})"
python3 -m vllm.entrypoints.openai.api_server \
--model "${model_path}" --served-model-name "${served_name}" \
--tensor-parallel-size "${TP_SIZE}" \
--gpu-memory-utilization "${GPU_MEM_UTIL}" --max-model-len "${MAX_MODEL_LEN}" \
--port "${VLLM_PORT}" ${extra} > "${LOG_DIR}/server_${log_tag}.log" 2>&1 &
SERVER_PID=$!
until curl -s "http://${SERVER_HOST}:${VLLM_PORT}/v1/models" > /dev/null 2>&1; do
if ! kill -0 "${SERVER_PID}" 2>/dev/null; then
echo "서버 기동 실패: ${LOG_DIR}/server_${log_tag}.log 확인 (오프라인 관련 에러인지 grep -i offline/resolve 로 확인)"
exit 1
fi
sleep 5
done
log "서버 준비 완료 (PID ${SERVER_PID})"
# --- 스모크 테스트: 실제 추론 요청 1건을 먼저 던져서, 벤치마크 수백 건이 전부
# 실패하기 전에 "진짜 에러 본문"을 즉시 확인. vllm bench serve의 요약 메시지
# ("misconfiguration on benchmark arguments")는 원인을 알려주지 않으므로 이게 더 빠름.
log "스모크 테스트 요청 전송 중..."
local smoke_resp smoke_http_code
smoke_resp=$(curl -s -w "\nHTTP_CODE:%{http_code}" "http://${SERVER_HOST}:${VLLM_PORT}/v1/chat/completions" \
-H "Content-Type: application/json" \
-d "{\"model\":\"${served_name}\",\"messages\":[{\"role\":\"user\",\"content\":\"hi\"}],\"max_tokens\":8}")
smoke_http_code=$(echo "${smoke_resp}" | grep -o 'HTTP_CODE:[0-9]*' | cut -d: -f2)
if [ "${smoke_http_code}" != "200" ]; then
log "경고: 스모크 요청이 HTTP ${smoke_http_code} 를 반환했습니다. 본문:"
echo "${smoke_resp}" | sed '/HTTP_CODE:/d' | tee "${LOG_DIR}/smoke_fail_${log_tag}.json"
log "위 detail 메시지가 원인입니다. 벤치마크는 계속 진행하지만 전부 실패할 가능성이 높습니다."
else
log "스모크 테스트 통과 (HTTP 200)"
fi
}
stop_vllm_server() {
kill -TERM "${SERVER_PID}" 2>/dev/null || true
wait "${SERVER_PID}" 2>/dev/null || true
sleep 5
}
run_serving_bench() {
# served_name: 서버가 --served-model-name 으로 노출한 이름 (요청 body의 "model" 필드로 사용됨)
# tokenizer_path: 토크나이저 로딩용 로컬 경로 (HF Hub 접근 방지의 핵심)
# prefix_len: 0이면 미사용, >0이면 --random-prefix-len 적용 (Prefix Caching 시나리오용)
local tag=$1 served_name=$2 tokenizer_path=$3 in_len=$4 out_len=$5 concurrency=$6 num_prompts=$7 prefix_len=${8:-0}
local prefix_arg=""
[ "${prefix_len}" -gt 0 ] && prefix_arg="--random-prefix-len ${prefix_len}"
# vLLM 최신 버전은 "vllm bench serve" CLI 서브커맨드를 사용 (구 모듈 경로
# vllm.entrypoints.openai.benchmarks.benchmark_serving 은 더 이상 존재하지 않음).
# --max-concurrency로 실제 동시 연결 수를 고정하고, --request-rate inf로 그 한도 내에서
# 최대한 밀어넣는 방식(포화 테스트)을 씀 — 예전의 --request-rate만으로 concurrency를
# 대신하던 방식보다 의미가 명확함.
vllm bench serve \
--backend openai-chat --endpoint /v1/chat/completions \
--host "${SERVER_HOST}" --port "${VLLM_PORT}" \
--model "${served_name}" --tokenizer "${tokenizer_path}" \
--dataset-name random --random-input-len "${in_len}" --random-output-len "${out_len}" ${prefix_arg} \
--num-prompts "${num_prompts}" --max-concurrency "${concurrency}" --request-rate inf \
--save-result --result-dir "${LOG_DIR}" --result-filename "${tag}.json" || true
}
# --- 1) Dense 70B, BF16, Prefix Caching 끔: Short/Long Context ---
start_vllm_server "${MODEL_70B}" "llama-3.1-70b" "none" "false" "70b_bf16"
for C in 1 16 64; do run_serving_bench "dense70b_bf16_short_c${C}" "llama-3.1-70b" "${MODEL_70B}" 1024 512 "${C}" $((C*5)); done
for C in 1 8 16; do run_serving_bench "dense70b_bf16_long_c${C}" "llama-3.1-70b" "${MODEL_70B}" 32768 512 "${C}" $((C*2)); done
stop_vllm_server
# --- 2) Dense 70B, FP8 양자화 ---
start_vllm_server "${MODEL_70B}" "llama-3.1-70b" "fp8" "false" "70b_fp8"
for C in 1 16 64; do run_serving_bench "dense70b_fp8_short_c${C}" "llama-3.1-70b" "${MODEL_70B}" 1024 512 "${C}" $((C*5)); done
for C in 1 8 16; do run_serving_bench "dense70b_fp8_long_c${C}" "llama-3.1-70b" "${MODEL_70B}" 32768 512 "${C}" $((C*2)); done
stop_vllm_server
# --- 3) Prefix Caching 검증 (32k 고정 프리픽스 반복 요청) ---
start_vllm_server "${MODEL_70B}" "llama-3.1-70b" "none" "true" "70b_prefixcache"
run_serving_bench "long_ctx_shared_prefix_cached" "llama-3.1-70b" "${MODEL_70B}" 32768 256 8 30 32000
stop_vllm_server
# --- 4) 대형 MoE 모델 DeepSeek-R1 FP8 ---
log "DeepSeek-R1 FP8 로딩은 시간이 오래 걸릴 수 있습니다(모델 크기 약 700GB)."
start_vllm_server "${MODEL_DEEPSEEK}" "deepseek-r1" "none" "false" "deepseek_r1"
for C in 1 8 16; do run_serving_bench "moe_deepseek_r1_fp8_short_c${C}" "deepseek-r1" "${MODEL_DEEPSEEK}" 1024 512 "${C}" $((C*3)); done
stop_vllm_server
# --- 5) 소형 8B 모델 ---
start_vllm_server "${MODEL_8B}" "llama-3.1-8b" "none" "false" "8b"
for C in 1 16 64 128; do run_serving_bench "small8b_short_c${C}" "llama-3.1-8b" "${MODEL_8B}" 1024 512 "${C}" $((C*5)); done
stop_vllm_server
log "=== 결과 집계 ==="
python3 - << PYEOF
import os, json, glob
from tabulate import tabulate
log_dir = "${LOG_DIR}"
rows = []
for fp in glob.glob(os.path.join(log_dir, "*.json")):
name = os.path.basename(fp).replace(".json","")
try:
d = json.load(open(fp))
rows.append([name, f"{d.get('median_ttft_ms',0):.2f}ms", f"{d.get('median_itl_ms',0):.2f}ms",
f"{d.get('request_throughput',0):.2f}req/s", f"{d.get('output_throughput',0):.2f}tok/s"])
except Exception:
continue
headers = ["Scenario","Median TTFT","Median ITL","Req/s","Tok/s"]
table = tabulate(sorted(rows), headers=headers, tablefmt="github")
open(f"{log_dir}/vllm_report.md","w").write("# vLLM Benchmark Report\n\n"+table+"\n")
print(table)
PYEOF
log "vLLM 벤치마크 완료. 리포트: ${LOG_DIR}/vllm_report.md"