26S29c

QK·4일 전
#!/usr/bin/env bash
# ==========================================================================
# STAGE 3a-8: Prefix Caching A/B 검증
#
# 목적:
#   1) Prefix Cache가 실제로 hit 되는지 확인
#   2) Cached token 수 확인
#   3) KV cache capacity / usage 확인
#   4) APC ON/OFF의 TTFT 차이 측정
#   5) benchmark가 동일 prefix를 사용하는지 검증
#
# 기본 workload:
#   32K fixed prefix + 32K random suffix + 256 output
#   총 input ~= 64K tokens/request
#
# 주의:
#   이 테스트에서는 64K 전체가 cache되는 것이 아니라
#   동일한 32K prefix 부분만 APC 대상이다.
# ==========================================================================

set -Eeuo pipefail

export HF_HUB_OFFLINE=1
export TRANSFORMERS_OFFLINE=1
export HF_DATASETS_OFFLINE=1

# --------------------------------------------------------------------------
# Configuration
# --------------------------------------------------------------------------

LOCAL_MODEL_ROOT="${LOCAL_MODEL_ROOT:-/mnt/local-nvme-cache/models}"
MODEL_70B="${MODEL_70B:-${LOCAL_MODEL_ROOT}/Meta-Llama-3.1-70B-Instruct}"

[ -d "${MODEL_70B}" ] || {
    echo "ERROR: 모델 없음: ${MODEL_70B}"
    exit 1
}

TP_SIZE="${TP_SIZE:-8}"
VLLM_PORT="${VLLM_PORT:-8000}"
SERVER_HOST="${SERVER_HOST:-127.0.0.1}"

# --------------------------------------------------------------------------
# Benchmark configuration
# --------------------------------------------------------------------------

DATASET_NAME="${DATASET_NAME:-random}"

# Fixed prefix
PREFIX_LEN="${PREFIX_LEN:-32000}"

# Random suffix
SUFFIX_LEN="${SUFFIX_LEN:-32768}"

# Output
OUTPUT_LEN="${OUTPUT_LEN:-256}"

NUM_PROMPTS="${NUM_PROMPTS:-30}"
MAX_CONCURRENCY="${MAX_CONCURRENCY:-8}"

# --------------------------------------------------------------------------
# Logging
# --------------------------------------------------------------------------

LOG_DIR="/var/log/b300_validation/stage3a8_prefixab_$(date +%Y%m%d_%H%M%S)"

mkdir -p "${LOG_DIR}"

source ~/vllm-bench-env/bin/activate

log() {
    echo -e "\n\033[1;32m[$(date '+%Y-%m-%d %H:%M:%S')] $1\033[0m"
}

warn() {
    echo -e "\033[1;33mWARNING: $1\033[0m"
}

error() {
    echo -e "\033[1;31mERROR: $1\033[0m"
}

# --------------------------------------------------------------------------
# Cleanup
# --------------------------------------------------------------------------

SERVER_PID=""
METRICS_PID=""

cleanup() {

    if [[ -n "${METRICS_PID}" ]]; then
        kill "${METRICS_PID}" 2>/dev/null || true
        METRICS_PID=""
    fi

    if [[ -n "${SERVER_PID}" ]]; then
        kill -TERM "${SERVER_PID}" 2>/dev/null || true
        wait "${SERVER_PID}" 2>/dev/null || true
        SERVER_PID=""
    fi
}

trap cleanup EXIT INT TERM

# --------------------------------------------------------------------------
# Check vLLM version
# --------------------------------------------------------------------------

log "vLLM version 확인"

python3 - <<'PYEOF' | tee "${LOG_DIR}/vllm_version.txt"
import vllm
print(vllm.__version__)
PYEOF

# --------------------------------------------------------------------------
# Check supported CLI options
# --------------------------------------------------------------------------

log "Prefix Cache 관련 CLI 확인"

python3 -m vllm.entrypoints.openai.api_server --help 2>&1 |
    grep -E \
    'enable-prefix-caching|prefix-match-unit|gpu-memory-utilization|max-model-len' \
    | tee "${LOG_DIR}/prefix_cache_cli_options.txt" || true

# --------------------------------------------------------------------------
# Start server
# --------------------------------------------------------------------------

start_server() {

    local tag="$1"
    local cache_flag="$2"

    SERVER_LOG="${LOG_DIR}/server_${tag}.log"
    METRICS_LOG="${LOG_DIR}/metrics_${tag}.log"

    log "============================================================"
    log "서버 기동: ${tag}"
    log "Prefix Cache: ${cache_flag}"
    log "============================================================"

    python3 -m vllm.entrypoints.openai.api_server \
        --model "${MODEL_70B}" \
        --served-model-name "llama-3.1-70b" \
        --tensor-parallel-size "${TP_SIZE}" \
        --gpu-memory-utilization 0.90 \
        --max-model-len 65536 \
        ${cache_flag} \
        --port "${VLLM_PORT}" \
        > "${SERVER_LOG}" 2>&1 &

    SERVER_PID=$!

    log "vLLM PID=${SERVER_PID}"

    # ----------------------------------------------------------------------
    # Wait for server
    # ----------------------------------------------------------------------

    log "vLLM API readiness 대기"

    local ready=0

    for i in $(seq 1 120); do

        if curl -fsS \
            "http://${SERVER_HOST}:${VLLM_PORT}/v1/models" \
            > /dev/null 2>&1; then

            ready=1
            break
        fi

        if ! kill -0 "${SERVER_PID}" 2>/dev/null; then
            error "vLLM 서버가 종료되었습니다."

            echo
            echo "===== SERVER LOG ====="
            tail -100 "${SERVER_LOG}"

            exit 1
        fi

        sleep 5
    done

    if [[ "${ready}" != "1" ]]; then

        error "vLLM 서버 readiness timeout"

        echo
        echo "===== SERVER LOG ====="
        tail -100 "${SERVER_LOG}"

        exit 1
    fi

    log "vLLM API ready"

    # ----------------------------------------------------------------------
    # Save model information
    # ----------------------------------------------------------------------

    curl -fsS \
        "http://${SERVER_HOST}:${VLLM_PORT}/v1/models" \
        > "${LOG_DIR}/models_${tag}.json"

    # ----------------------------------------------------------------------
    # Save initial metrics
    # ----------------------------------------------------------------------

    curl -fsS \
        "http://${SERVER_HOST}:${VLLM_PORT}/metrics" \
        > "${LOG_DIR}/metrics_${tag}_before.txt"

    # ----------------------------------------------------------------------
    # Start metrics collector
    # ----------------------------------------------------------------------

    log "Runtime metrics 수집 시작"

    (
        while true; do

            {
                echo
                echo "============================================================"
                echo "TIMESTAMP=$(date '+%Y-%m-%d %H:%M:%S')"
                echo "============================================================"

                curl -fsS \
                    "http://${SERVER_HOST}:${VLLM_PORT}/metrics" 2>/dev/null |
                    grep -E \
                    'prefix_cache_(queries|hits)|prompt_tokens_cached|kv_cache_usage_perc|gpu_cache_usage_perc' \
                    || true

            } >> "${METRICS_LOG}"

            sleep 1

        done
    ) &

    METRICS_PID=$!

    # ----------------------------------------------------------------------
    # Show KV cache related startup information
    # ----------------------------------------------------------------------

    log "KV cache startup 정보"

    grep -Ei \
        'KV cache|GPU KV|num_gpu_blocks|gpu.*block|cache size|block' \
        "${SERVER_LOG}" \
        | tail -30 \
        | tee "${LOG_DIR}/kv_cache_startup_${tag}.txt" \
        || true
}

# --------------------------------------------------------------------------
# Run benchmark
# --------------------------------------------------------------------------

run_benchmark() {

    local tag="$1"

    local RESULT_FILE="${LOG_DIR}/${tag}.json"

    log "============================================================"
    log "Benchmark 시작: ${tag}"
    log "============================================================"

    log "Workload:"
    log "  Dataset       = ${DATASET_NAME}"
    log "  Fixed prefix  = ${PREFIX_LEN}"
    log "  Random suffix = ${SUFFIX_LEN}"
    log "  Output        = ${OUTPUT_LEN}"
    log "  Prompts       = ${NUM_PROMPTS}"
    log "  Concurrency   = ${MAX_CONCURRENCY}"
    log "  Approx input  = $((PREFIX_LEN + SUFFIX_LEN)) tokens"
    log "============================================================"

    # ----------------------------------------------------------------------
    # Benchmark
    # ----------------------------------------------------------------------

    vllm bench serve \
        --backend openai-chat \
        --endpoint /v1/chat/completions \
        --host "${SERVER_HOST}" \
        --port "${VLLM_PORT}" \
        --model "llama-3.1-70b" \
        --tokenizer "${MODEL_70B}" \
        --dataset-name "${DATASET_NAME}" \
        --random-input-len "${SUFFIX_LEN}" \
        --random-output-len "${OUTPUT_LEN}" \
        --random-prefix-len "${PREFIX_LEN}" \
        --num-prompts "${NUM_PROMPTS}" \
        --max-concurrency "${MAX_CONCURRENCY}" \
        --request-rate inf \
        --save-result \
        --result-dir "${LOG_DIR}" \
        --result-filename "${tag}.json"

    # ----------------------------------------------------------------------
    # Verify result
    # ----------------------------------------------------------------------

    if [[ ! -s "${RESULT_FILE}" ]]; then
        error "Benchmark result 없음: ${RESULT_FILE}"
        exit 1
    fi

    # ----------------------------------------------------------------------
    # Save final metrics
    # ----------------------------------------------------------------------

    curl -fsS \
        "http://${SERVER_HOST}:${VLLM_PORT}/metrics" \
        > "${LOG_DIR}/metrics_${tag}_after.txt"

    # ----------------------------------------------------------------------
    # Stop metrics collector
    # ----------------------------------------------------------------------

    if [[ -n "${METRICS_PID}" ]]; then
        kill "${METRICS_PID}" 2>/dev/null || true
        METRICS_PID=""
    fi

    log "Benchmark 완료: ${tag}"

    # ----------------------------------------------------------------------
    # Show cache metrics
    # ----------------------------------------------------------------------

    echo
    echo "===== ${tag}: Prefix Cache Metrics ====="

    grep -E \
        'prefix_cache_(queries|hits)|prompt_tokens_cached|kv_cache_usage_perc|gpu_cache_usage_perc' \
        "${LOG_DIR}/metrics_${tag}_after.txt" \
        || true
}

# --------------------------------------------------------------------------
# Stop server
# --------------------------------------------------------------------------

stop_server() {

    log "vLLM 서버 종료"

    if [[ -n "${METRICS_PID}" ]]; then
        kill "${METRICS_PID}" 2>/dev/null || true
        METRICS_PID=""
    fi

    if [[ -n "${SERVER_PID}" ]]; then

        kill -TERM "${SERVER_PID}" 2>/dev/null || true

        wait "${SERVER_PID}" 2>/dev/null || true

        SERVER_PID=""

    fi

    sleep 5
}

# ==========================================================================
# CASE 1: Prefix Cache ON
# ==========================================================================

run_case_on() {

    start_server \
        "prefix_cache_on" \
        "--enable-prefix-caching"

    run_benchmark \
        "prefix_cache_on"

    stop_server
}

# ==========================================================================
# CASE 2: Prefix Cache OFF
# ==========================================================================

run_case_off() {

    start_server \
        "prefix_cache_off" \
        "--no-enable-prefix-caching"

    run_benchmark \
        "prefix_cache_off"

    stop_server
}

# ==========================================================================
# Execute A/B
# ==========================================================================

log "============================================================"
log "STAGE 3a-8 Prefix Caching A/B"
log "============================================================"

log "Model       : ${MODEL_70B}"
log "TP          : ${TP_SIZE}"
log "Prefix      : ${PREFIX_LEN}"
log "Suffix      : ${SUFFIX_LEN}"
log "Output      : ${OUTPUT_LEN}"
log "Prompts     : ${NUM_PROMPTS}"
log "Concurrency : ${MAX_CONCURRENCY}"
log "Log Dir     : ${LOG_DIR}"

run_case_on

run_case_off

# ==========================================================================
# Result Analysis
# ==========================================================================

log "=== 결과 분석 ==="

python3 - "${LOG_DIR}" <<'PYEOF'

import json
import os
import re
import statistics
import sys

log_dir = sys.argv[1]


# --------------------------------------------------------------------------
# JSON loader
# --------------------------------------------------------------------------

def load_json(tag):

    path = os.path.join(log_dir, f"{tag}.json")

    if not os.path.exists(path):
        return None

    try:
        with open(path) as f:
            return json.load(f)
    except Exception as e:
        print(f"WARNING: JSON parse failed: {path}: {e}")
        return None


# --------------------------------------------------------------------------
# Metrics parser
# --------------------------------------------------------------------------

def parse_metrics(path):

    result = {
        "prefix_cache_queries": None,
        "prefix_cache_hits": None,
        "prompt_tokens_cached": None,
        "kv_cache_usage_perc": None,
    }

    if not os.path.exists(path):
        return result

    with open(path, errors="ignore") as f:
        text = f.read()

    patterns = {
        "prefix_cache_queries":
            r'(?:vllm:)?prefix_cache_queries(?:\{[^}]*\})?\s+([0-9.eE+-]+)',

        "prefix_cache_hits":
            r'(?:vllm:)?prefix_cache_hits(?:\{[^}]*\})?\s+([0-9.eE+-]+)',

        "prompt_tokens_cached":
            r'(?:vllm:)?prompt_tokens_cached(?:\{[^}]*\})?\s+([0-9.eE+-]+)',

        "kv_cache_usage_perc":
            r'(?:vllm:)?kv_cache_usage_perc(?:\{[^}]*\})?\s+([0-9.eE+-]+)',
    }

    for key, pattern in patterns.items():

        matches = re.findall(pattern, text)

        if matches:
            try:
                result[key] = float(matches[-1])
            except ValueError:
                pass

    return result


# --------------------------------------------------------------------------
# Startup log parser
# --------------------------------------------------------------------------

def parse_startup(tag):

    path = os.path.join(
        log_dir,
        f"kv_cache_startup_{tag}.txt"
    )

    if not os.path.exists(path):
        return []

    with open(path, errors="ignore") as f:
        return [
            line.strip()
            for line in f
            if line.strip()
        ]


# --------------------------------------------------------------------------
# Benchmark JSON
# --------------------------------------------------------------------------

on = load_json("prefix_cache_on")
off = load_json("prefix_cache_off")

metrics_on = parse_metrics(
    os.path.join(
        log_dir,
        "metrics_prefix_cache_on_after.txt"
    )
)

metrics_off = parse_metrics(
    os.path.join(
        log_dir,
        "metrics_prefix_cache_off_after.txt"
    )
)

startup_on = parse_startup("prefix_cache_on")
startup_off = parse_startup("prefix_cache_off")


# --------------------------------------------------------------------------
# Extract benchmark values
# --------------------------------------------------------------------------

def value(data, key):

    if not data:
        return None

    value = data.get(key)

    if isinstance(value, (int, float)):
        return value

    return None


ttft_on = value(on, "median_ttft_ms")
ttft_off = value(off, "median_ttft_ms")

e2e_on = value(on, "median_e2e_latency_ms")
e2e_off = value(off, "median_e2e_latency_ms")

tpot_on = value(on, "median_tpot_ms")
tpot_off = value(off, "median_tpot_ms")

input_tps_on = value(on, "input_throughput")
input_tps_off = value(off, "input_throughput")

output_tps_on = value(on, "output_throughput")
output_tps_off = value(off, "output_throughput")


# --------------------------------------------------------------------------
# Calculate cache hit ratio
# --------------------------------------------------------------------------

queries = metrics_on["prefix_cache_queries"]
hits = metrics_on["prefix_cache_hits"]

hit_ratio = None

if queries is not None and queries > 0 and hits is not None:
    hit_ratio = hits / queries * 100.0


# --------------------------------------------------------------------------
# Cached tokens / request
# --------------------------------------------------------------------------

cached_tokens_per_request = None

cached_tokens = metrics_on["prompt_tokens_cached"]

if cached_tokens is not None:

    # This is an approximate interpretation because
    # prompt_tokens_cached is a cumulative metric.
    cached_tokens_per_request = (
        cached_tokens / 30.0
    )


# --------------------------------------------------------------------------
# Speedup
# --------------------------------------------------------------------------

ttft_speedup = None

if ttft_on and ttft_on > 0 and ttft_off:
    ttft_speedup = ttft_off / ttft_on


e2e_speedup = None

if e2e_on and e2e_on > 0 and e2e_off:
    e2e_speedup = e2e_off / e2e_on


# --------------------------------------------------------------------------
# Markdown report
# --------------------------------------------------------------------------

report = []

report.append(
    "# Stage 3a-8 Prefix Caching A/B 결과"
)

report.append("")

report.append(
    "## Workload"
)

report.append("")

report.append(
    "| 항목 | 값 |"
)

report.append("|---|---:|")

report.append(
    "| Fixed Prefix | 32,000 tokens |"
)

report.append(
    "| Random Suffix | 32,768 tokens |"
)

report.append(
    "| Approx Input | ~64,768 tokens |"
)

report.append(
    "| Output | 256 tokens |"
)

report.append(
    "| Requests | 30 |"
)

report.append(
    "| Concurrency | 8 |"
)

report.append("")

report.append(
    "## Latency"
)

report.append("")

report.append(
    "| Metric | APC OFF | APC ON | Improvement |"
)

report.append("|---|---:|---:|---:|")


def fmt(x):

    if x is None:
        return "N/A"

    return f"{x:.2f}"


def ratio(off_value, on_value):

    if (
        off_value is None
        or on_value is None
        or on_value == 0
    ):
        return "N/A"

    return f"{off_value / on_value:.2f}x"


report.append(
    f"| Median TTFT | {fmt(ttft_off)} ms | "
    f"{fmt(ttft_on)} ms | "
    f"{ratio(ttft_off, ttft_on)} |"
)

report.append(
    f"| Median E2E | {fmt(e2e_off)} ms | "
    f"{fmt(e2e_on)} ms | "
    f"{ratio(e2e_off, e2e_on)} |"
)

report.append(
    f"| Median TPOT | {fmt(tpot_off)} ms | "
    f"{fmt(tpot_on)} ms | "
    f"{ratio(tpot_off, tpot_on)} |"
)

report.append(
    f"| Input throughput | {fmt(input_tps_off)} | "
    f"{fmt(input_tps_on)} | "
    f"{ratio(input_tps_on, input_tps_off)} |"
)

report.append(
    f"| Output throughput | {fmt(output_tps_off)} | "
    f"{fmt(output_tps_on)} | "
    f"{ratio(output_tps_on, output_tps_off)} |"
)

report.append("")

report.append(
    "## Prefix Cache Evidence"
)

report.append("")

report.append(
    "| Metric | APC OFF | APC ON |"
)

report.append("|---|---:|---:|")

report.append(
    f"| Prefix cache queries | "
    f"{fmt(metrics_off['prefix_cache_queries'])} | "
    f"{fmt(metrics_on['prefix_cache_queries'])} |"
)

report.append(
    f"| Prefix cache hits | "
    f"{fmt(metrics_off['prefix_cache_hits'])} | "
    f"{fmt(metrics_on['prefix_cache_hits'])} |"
)

report.append(
    f"| Cached prompt tokens | "
    f"{fmt(metrics_off['prompt_tokens_cached'])} | "
    f"{fmt(metrics_on['prompt_tokens_cached'])} |"
)

report.append(
    f"| KV cache usage | "
    f"{fmt(metrics_off['kv_cache_usage_perc'])} | "
    f"{fmt(metrics_on['kv_cache_usage_perc'])} |"
)

report.append("")

report.append(
    "## APC ON 해석"
)

report.append("")

if queries is None or hits is None:

    report.append(
        "⚠️ Prefix cache hit/query metric을 확인하지 못했습니다. "
        "vLLM version 또는 metric name을 확인해야 합니다."
    )

else:

    report.append(
        f"- Prefix cache queries: {queries:,.0f}"
    )

    report.append(
        f"- Prefix cache hits: {hits:,.0f}"
    )

    report.append(
        f"- Cache hit ratio: {hit_ratio:.2f}%"
    )

    if cached_tokens is not None:

        report.append(
            f"- Cached prompt tokens: {cached_tokens:,.0f}"
        )

    if hit_ratio is not None and hit_ratio > 0:

        report.append(
            "- Prefix Cache가 실제로 hit된 evidence가 확인됩니다."
        )

    else:

        report.append(
            "- Prefix Cache hit evidence가 확인되지 않습니다."
        )

report.append("")

report.append(
    "## 중요 해석"
)

report.append("")

report.append(
    "이 테스트는 약 64.8K input 중 약 32K fixed prefix만 "
    "Prefix Cache 대상입니다. 따라서 APC ON에서 5~10x TTFT "
    "개선을 사전에 정상 기준으로 가정하지 않습니다."
)

report.append("")

report.append(
    "APC 효과는 반드시 cache hit/cached tokens와 TTFT를 함께 "
    "판단해야 합니다."
)

report.append("")

report.append(
    "## KV Cache Startup Information"
)

report.append("")

for line in startup_on:
    report.append(f"`{line}`")

report.append("")

report.append(
    "## Files"
)

report.append("")

report.append(
    "### Server logs"
)

report.append(
    f"- `{os.path.join(log_dir, 'server_prefix_cache_on.log')}`"
)

report.append(
    f"- `{os.path.join(log_dir, 'server_prefix_cache_off.log')}`"
)

report.append("")

report.append(
    "### Metrics"
)

report.append(
    f"- `{os.path.join(log_dir, 'metrics_prefix_cache_on_after.txt')}`"
)

report.append(
    f"- `{os.path.join(log_dir, 'metrics_prefix_cache_off_after.txt')}`"
)

report_text = "\n".join(report)

report_path = os.path.join(
    log_dir,
    "prefix_cache_ab_report.md"
)

with open(report_path, "w") as f:
    f.write(report_text)

print()
print(report_text)
print()
print(f"Report: {report_path}")

PYEOF

log "============================================================"
log "Prefix Caching A/B 완료"
log "============================================================"

log "결과 디렉터리:"
echo "${LOG_DIR}"

log "Report:"
echo "${LOG_DIR}/prefix_cache_ab_report.md"
profile
engineer

0개의 댓글