vLLM 生产调优实战:PagedAttention、KV Cache 与请求调度
适用场景:vLLM 服务已经跑起来,但吞吐上不去、延迟抖成心电图、显存总差那么一点。这篇不是部署教程,是把推理引擎内部榨干的调优笔记。
1. 先说那个凌晨两点的告警
我遇到过最诡异的一次:凌晨两点告警,GPU 利用率 100%,吞吐量却是 0。四张 A100 全在烧,日志里每一行都是 preemption,请求全排队,没有一个能出 token。重启三回还是这样。最后冷静看调度器日志才明白——三天前我把 --max-num-seqs 从 128 调到 512,想"把并发拉满"。KV Cache 池装不下这么多序列,调度器疯狂抢占,GPU 在空转。
2. 先搞懂 PagedAttention:KV Cache 不是缓存,是显存池
在 vLLM 里 KV Cache 是主显存消耗者,比权重占得多。传统框架为每个请求预留 max_model_len 连续显存,一个 8k 请求独占 8k 槽位。vLLM 的 PagedAttention 把它当虚拟内存管:KV Cache 切成固定大小 block(默认 16 token),用 block table 索引,物理上不连续也行。
传统框架(预留式) vLLM(PagedAttention)
┌─────────────┐ ┌──────┬──────┬──────┬──────┐
│ 预留 8k │ │ blk0 │ blk1 │ blk2 │ ... │ ← 空闲池
│ 实际用 100 │ └──┬───┴──┬───┴──┬───┴──┬───┘
│ 浪费 7900 │ │ │ │ │
└─────────────┘ block table: [0→blk3, 1→blk7, ...]
3. 三个显存参数的三角平衡
启动时算:KV 池 = 总显存 × gpu-memory-utilization − 权重 − 激活。三个参数互相咬合,动一个,另两个就变。
3.1 --gpu-memory-utilization:给 KV 池留多少
默认 0.9。我调过的范围:
| 值 | 表现 | 我的结论 |
|---|---|---|
| 0.80 | stable, small KV pool | too conservative |
| 0.90 | default, most cases ok | safe start |
| 0.95 | big KV, thin headroom | test first, long seq OOM |
我踩过的坑:0.95 配 --max-model-len 32768,一个 20k prompt 进来,激活内存撑爆,CUDA OOM 杀了 worker。0.90 起步,别贪。
3.2 --max-model-len:单请求的生命线
它限制输入 + 输出最大 token 数。我常犯的错是"模型支持 128k 就设 128k",但序列 block 上限按 max_model_len 预留——设越大,能同时跑的序列越少。
经验公式:可容纳序列数 ≈ KV 池 token 总数 ÷ max_model_len。池子 50 万 token,max-model-len 32768 约 15 个长序列。
3.3 --kv-cache 与 --kv-cache-dtype
这两个是 KV 的"容量与精度"开关:
# 显存吃紧时,KV Cache 用 FP8,能省一半空间(质量损失可控)
--kv-cache-dtype fp8
# 手动指定 KV 池大小(token 数),通常不推荐,让 vLLM 自己算
--kv-cache 500000
3.4 参数速查矩阵
| 参数 | 默认 | 推荐 | 影响 |
|---|---|---|---|
--gpu-memory-utilization |
0.9 | 0.90 起步 | KV 池大小 |
--max-model-len |
4096 | 16384 | 单请求上限 |
--max-num-seqs |
256 | 128 | 并发 batch 上限 |
--kv-cache-dtype |
auto | fp8 | KV 精度/容量 |
--chunked-prefill-size |
8192 | 4096 | TTFT 稳定性 |
--preemption-mode |
swap | swap | 抢占策略 |
--enable-prefix-caching |
off | on | 前缀复用 |
4. 并发与调度:continuous batching 的真相
vLLM 的吞吐靠 continuous batching:不是"来一批处理一批",而是每一步都动态调整 batch——新请求进、完成的出。--max-num-seqs 是 batch 硬上限,受两个约束:调度器上限(你设的值)和 KV 池上限(显存真实能容纳的序列数)。
实际并发 = min(调度器上限, KV 池上限)。我见过太多人把 --max-num-seqs 设 512,实际被 KV 池卡在 40。设大没用,还会引发抢占。调度器日志:
# vLLM 启动日志里会打印 KV 池 token 数
grep -i "kv cache" /var/log/vllm.log
# 实时观测正在跑的序列数与 KV 池占用
watch -n 2 "grep -oE 'running=[0-9]+|pending=[0-9]+' /var/log/vllm.log | tail -5"
5. preemption:调度器里的隐藏杀手
preemption发生在 KV 池满,新请求 prefill 需空间,调度器被迫驱逐 running 序列。两条路:
- swap:KV block 搬到 CPU 内存再搬回,快但 CPU 带宽是瓶颈。
- recompute:丢掉 KV 从头重算,省内存但白算。
vLLM 默认 swap 优先(--preemption-mode swap)。判断是否被抢占,我写了个小脚本:
#!/usr/bin/env bash
# watch_preemption.sh —— 抢占风暴一眼看穿
LOG=${1:-/var/log/vllm.log}
while true; do
pre=$(grep -c "preempt" "$LOG" 2>/dev/null)
run=$(grep -oE "running=[0-9]+" "$LOG" | tail -1)
pen=$(grep -oE "pending=[0-9]+" "$LOG" | tail -1)
gpu=$(nvidia-smi --query-gpu=utilization.gpu --format=csv,noheader,nounits | head -1)
echo "$(date +%H:%M:%S) preempt=$pre $run $pen gpu=${gpu}%"
sleep 5
done
抢占风暴信号:GPU 90%+,每步都在 preempt,吞吐反而塌方。三步走:
- 降
--max-num-seqs:让 running 序列回到 KV 池承载范围。最直接。 - 升
--gpu-memory-utilization:给 KV 池扩容(0.90 → 0.92,小幅)。 - 开
--enable-chunked-prefill:减小单次 prefill 内存尖峰,给调度器腾挪空间。
6. chunked prefill:首 token 延迟的解药
--enable-chunked-prefill 把长 prefill 切成 chunk(默认 8192 token),插到 decode 步之间。没有它,一个 8k prompt 的大请求占满 GPU 算完 prefill,后续短请求的 TTFT 从 200ms 飙到 3 秒——我遇到过用户发工单:"为什么聊天机器人打字先卡三秒?"
调 chunked-prefill-size 的经验:4096 是稳定点,8192 在长 prompt 多时调度更高效。短请求多选小,长 prompt 多选大。
7. 五个我遇到过的真实事故
事故一:启动即 OOM
现象:CUDA out of memory,起不来。根因:0.95 + max-model-len 32768,KV 池初始化超了。
事故二:preemption 风暴(就是开头那个)
现象:GPU 100%,吞吐 0,日志全是 preempt。根因:--max-num-seqs 512 超 KV 池承载。
事故三:首 token 延迟飙升
现象:P50 TTFT 正常,P99 从 300ms 变 4s。根因:16k prompt 批量任务占住 GPU。
开完 P99 TTFT 回到 500ms 内。长 prompt 与大并发共存,chunked prefill 必开。事故四:并发死活上不去
现象:--max-num-seqs 256,但并发稳定在 30 上下。根因:KV 池 token 总数 ÷ max-model-len 32768 ≈ 30,KV 池才是瓶颈。
事故五:显存缓慢泄漏
现象:显存一天涨 1GB,三天后 OOM,请求 503。根因:升级后,--enable-prefix-caching 共享 block 引用计数异常。
7.5 排障工具箱
#!/usr/bin/env bash
# diagnose-vllm.sh —— 一键定位抢占/OOM/延迟三类问题
set -uo pipefail
LOG=${1:-/var/log/vllm.log}
echo "== 1. KV 池与序列上限 =="
grep -iE "kv cache|max num|max model" "$LOG" | head -5
echo "== 2. 抢占计数 =="
pre=$(grep -c "preempt" "$LOG")
echo "累计 preempt 次数: $pre"
if [ "$pre" -gt 100 ]; then
echo ">>> 疑似抢占风暴,先降 --max-num-seqs"
fi
echo "== 3. OOM 痕迹 =="
grep -iE "out of memory|oom" "$LOG" | tail -3
echo "== 4. 显存与利用率 =="
nvidia-smi --query-gpu=utilization.gpu,memory.used,memory.total \
--format=csv,noheader
echo "== 5. 运行中序列数 =="
grep -oE "running=[0-9]+" "$LOG" | tail -1
grep -oE "pending=[0-9]+" "$LOG" | tail -1
7.6 上线后:监控告警配置
调优不是一次性的,关键指标接进 Prometheus:
#!/usr/bin/env bash
# monitor-vllm.sh —— 输出 Prometheus 指标,供 pushgateway 采集
LOG=${1:-/var/log/vllm.log}
kv=$(grep -iE "kv cache.*[0-9,]+ tokens" "$LOG" | grep -oE "[0-9,]+" | tail -1 | tr -d ',')
pre=$(grep -c "preempt" "$LOG")
run=$(grep -oE "running=[0-9]+" "$LOG" | tail -1 | grep -oE "[0-9]+")
pen=$(grep -oE "pending=[0-9]+" "$LOG" | tail -1 | grep -oE "[0-9]+")
mem=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits | head -1)
echo "vllm_kv_cache_tokens $kv"
echo "vllm_preemptions_total $pre"
echo "vllm_running_sequences $run"
echo "vllm_pending_sequences $pen"
echo "gpu_memory_used_bytes $((mem * 1024 * 1024))"
Prometheus 里两条告警规则:
# alert-rules.yml
groups:
- name: vllm
rules:
- alert: VllmPreemptionStorm
expr: rate(vllm_preemptions_total[5m]) > 5
for: 2m
labels: { severity: critical }
annotations:
summary: "抢占风暴,吞吐正在塌方,先降 max-num-seqs"
- alert: GpuMemLeak
expr: derivative(gpu_memory_used_bytes[1h]) > 1073741824
for: 3h
labels: { severity: warning }
annotations:
summary: "显存每小时涨超 1GB,疑似泄漏,盯 prefix caching"
8. 一个完整的生产启动参数模板
这是 8B 模型(A100 40G 单卡)的基准模板:
docker run --gpus all --shm-size 16g -p 8000:8000 \
-e VLLM_USE_PREFILL=False \
vllm/vllm-openai:latest \
/bin/sh -c "vllm serve meta-llama/Meta-Llama-3.1-8B \
--host 0.0.0.0 --port 8000 \
--max-model-len 16384 \
--gpu-memory-utilization 0.90 \
--max-num-seqs 128 \
--enable-chunked-prefill \
--chunked-prefill-size 4096 \
--kv-cache-dtype fp8 \
--enable-prefix-caching \
--disable-custom-all-reduce"
要点:shm-size 别省,NCCL 要共享内存;VLLM_USE_PREFILL=False 关 prefill 预取。
8.1 用 docker-compose 管理更清晰
# docker-compose.yml
services:
vllm:
image: vllm/vllm-openai:latest
command: >
vllm serve meta-llama/Meta-Llama-3.1-8B
--host 0.0.0.0 --port 8000
--max-model-len 16384
--gpu-memory-utilization 0.90
--max-num-seqs 128
--enable-chunked-prefill
--chunked-prefill-size 4096
--kv-cache-dtype fp8
--enable-prefix-caching
ports: ["8000:8000"]
shm_size: "16gb"
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
restart: unless-stopped
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 5s
retries: 5
8.2 多卡(TP)额外配置
多卡用 TP 时,NCCL 通信别用默认的:
# 4 卡 TP 启动片段
docker run --gpus all --shm-size 16g -p 8000:8000 \
-e NCCL_IB_DISABLE=0 \
-e NCCL_SHM_DISABLE=0 \
-e VLLM_USE_PREFILL=False \
vllm/vllm-openai:latest \
/bin/sh -c "vllm serve meta-llama/Meta-Llama-3.1-8B \
--tensor-parallel-size 4 \
--max-model-len 16384 \
--gpu-memory-utilization 0.90 \
--max-num-seqs 128 \
--enable-chunked-prefill \
--disable-custom-all-reduce"
9. 怎么证明调对了:压测方法论
每次调参前后各跑一遍,对比四指标:
# 用 openai 兼容模式压测,模拟 1000 个请求,并发 64
python -m vllm.bench_serving \
--model meta-llama/Meta-Llama-3.1-8B \
--endpoint /v1/completions \
--request-rate 20 \
--max-concurrency 64 \
--num-prompts 1000 \
--tokenizer meta-llama/Meta-Llama-3.1-8B
读输出四指标:
| 指标 | 含义 | 调优目标 |
|---|---|---|
| TTFT | 首 token 延迟 | 越低越稳,P99 别超 500ms |
| TPOT | 每个 token 平均延迟 | 稳定,别随并发飙升 |
| throughput | token/s 或 req/s | 越高越好 |
| preemption 次数 | 抢占次数 | 必须接近 0 |
9.1 找拐点:并发阶梯压测
一次压测不够,我用并发阶梯找"吞吐拐点":
for c in 8 16 32 64 128; do
echo "=== concurrency=$c ==="
python -m vllm.bench_serving \
--model meta-llama/Meta-Llama-3.1-8B \
--max-concurrency $c \
--num-prompts 500 \
--request-rate 100 2>&1 | grep -E "Throughput|TTFT|preemption"
done
9.2 想更细?自己写脚本采样
bench_serving 给聚合值,我偶尔写脚本逐请求采样 TTFT/TPOT:
# benchmark_tail.py —— 逐请求延迟分布
import asyncio, json, time, aiohttp, statistics
URL = "http://localhost:8000/v1/completions"
MODEL = "meta-llama/Meta-Llama-3.1-8B"
CONCURRENCY = 64
TOTAL = 500
async def one(session, sem):
async with sem:
t0 = time.perf_counter()
async with session.post(URL, json={
"model": MODEL,
"prompt": "写一段 200 字的技术说明。",
"max_tokens": 128,
}) as r:
data = await r.json()
ttft = data.get("timing", {}).get("first_token_ms")
total = (time.perf_counter() - t0) * 1000
return ttft, total
async def main():
sem = asyncio.Semaphore(CONCURRENCY)
async with aiohttp.ClientSession() as s:
res = await asyncio.gather(*[one(s, sem) for _ in range(TOTAL)])
ttfts = sorted(x[0] for x in res if x[0])
def p(pct):
i = int(len(ttfts) * pct)
return round(ttfts[min(i, len(ttfts)-1)], 1)
print(f"TTFT p50={p(0.5)}ms p99={p(0.99)}ms "
f"tp={TOTAL/sum(x[1] for x in res):.1f} req/s")
asyncio.run(main())
10. 调优检查清单
-
gpu-memory-utilization0.90 起步,压测稳定后再微调 -
max-model-len贴合真实业务,别按模型上限设 -
max-num-seqs先看 KV 池承载,别拍脑袋 - 长 prompt 场景必开
--enable-chunked-prefill - 显存吃紧用
kv-cache-dtype fp8,别硬扛 - 升级 vLLM 后盯 48h 显存曲线
- 每次调参用
bench_serving对比,preemption 为 0 才算赢
FAQ
附录:真实观测输出长这样
调优靠数据说话,我贴几段真实观测:
A.1 启动时的 KV 池日志
INFO 09:31:02 scheduler.py:214] Starting engine...
INFO 09:31:02 cache_config.py:118] GPU KV cache size: 512,000 tokens
INFO 09:31:02 cache_config.py:122] CPU offload KV cache size: 65,536 tokens
INFO 09:31:02 core.py:180] Maximum concurrency for 16,384 tokens: 128 sequences
INFO 09:31:02 scheduler.py:301] Initializing scheduler with max_num_seqs=128
A.2 抢占风暴时的调度器日志
WARNING scheduler.py:512] Preempting seq_id=1021 (swap out, 512 blocks)
WARNING scheduler.py:512] Preempting seq_id=1024 (swap out, 480 blocks)
WARNING scheduler.py:517] Preempting seq_id=1029 (recompute, 300 blocks)
INFO scheduler.py:388] running=128 pending=512 gpu_cache=0%
INFO scheduler.py:390] preemption count: 45 in last 5 minutes
A.3 bench_serving 输出
Number of requests: 1000
Concurrency: 64
Throughput: 1,234.56 requests/s, 156,789 tokens/s
TTFT: p50=45.2ms p90=120.4ms p99=289.1ms
TPOT: p50=8.1ms p99=15.3ms
A.4 Prometheus 查询(Grafana 用)
# 抢占速率,>5 触发告警
rate(vllm_preemptions_total[5m])
# KV 池占用率,>0.98 预判抢占
vllm_running_sequences / vllm_kv_cache_tokens
# 显存泄漏判据
derivative(gpu_memory_used_bytes[1h])
A.5 start.sh
#!/usr/bin/env bash
# start.sh —— vLLM 生产启动(8B / A100 40G 单卡)
set -euo pipefail
MODEL=${MODEL:-meta-llama/Meta-Llama-3.1-8B}
PORT=${PORT:-8000}
docker run -d --name vllm --gpus all --shm-size 16g -p ${PORT}:8000 -e VLLM_USE_PREFILL=False vllm/vllm-openai:latest /bin/sh -c "vllm serve ${MODEL} --host 0.0.0.0 --port 8000 --max-model-len 16384 --gpu-memory-utilization 0.90 --max-num-seqs 128 --enable-chunked-prefill --chunked-prefill-size 4096 --kv-cache-dtype fp8 --enable-prefix-caching"
echo "waiting for health..."
for i in $(seq 1 60); do
if curl -sf http://localhost:${PORT}/health >/dev/null; then
echo "vllm healthy in ${i}s"; exit 0
fi
sleep 2
done
echo "timeout waiting vllm"; exit 1
A.6 promtail pipeline
# promtail.yaml
scrape_configs:
- job_name: vllm
static_configs:
- targets: [localhost]
labels: { job: vllm }
pipeline_stages:
- docker: {}
- regex:
expression: "preemption count: (?P<pre>[0-9]+)"
- metrics:
- counter:
source: pre
name: vllm_preemptions_total
help: preemption count
A.7 healthcheck & shutdown
#!/usr/bin/env bash
# healthcheck.sh —— 探活 + 优雅停机
if curl -sf http://localhost:8000/health; then
exit 0
fi
# 停机前先摘流,再发 SIGTERM 等 vLLM 落盘
kill -TERM $(pgrep -f "vllm serve") && sleep 30 && kill -KILL $(pgrep -f "vllm serve")
exit 1
A.8 grafana dashboard
{
"title": "vLLM KV Cache & Preemption",
"panels": [
{
"type": "timeseries", "title": "preemptions rate",
"targets": [{"expr": "rate(vllm_preemptions_total[5m])"}]
},
{
"type": "timeseries", "title": "KV pool usage",
"targets": [{"expr": "vllm_running_sequences / vllm_kv_cache_tokens"}]
},
{
"type": "timeseries", "title": "gpu mem leak check",
"targets": [{"expr": "derivative(gpu_memory_used_bytes[1h])"}]
}
]
}
A.9 before/after comparison
BEFORE (naive) AFTER (tuned)
max-num-seqs 512 128
max-model-len 32768 16384
kv-cache-dtype auto fp8
preemption/5min 45 0
TTFT p99 4.2s 480ms
throughput 620 req/s 1,410 req/s
GPU util 100% (spinning) 96% (working)
A.10 Dockerfile & env
# Dockerfile —— 固定 vLLM 版本,避免升级踩坑
FROM vllm/vllm-openai:latest
ENV VLLM_USE_PREFILL=False NCCL_IB_DISABLE=0 NCCL_SHM_DISABLE=0
EXPOSE 8000
# .env —— 启动参数集中管理
MODEL=meta-llama/Meta-Llama-3.1-8B
MAX_MODEL_LEN=16384
GPU_MEM_UTIL=0.90
MAX_NUM_SEQS=128
CHUNKED_PREFILL_SIZE=4096
KV_CACHE_DTYPE=fp8
A.11 bench_serving 完整输出
------------------ Metrics -------------------
Total requests: 1000
Completed: 1000
Concurrency: 64
Request rate: 20.0 req/s
Throughput: 1,410.2 requests/s, 168,900 tokens/s
TTFT: p50=42.1ms, p90=118.5ms, p99=480.3ms
TPOT: p50=8.0ms, p90=12.1ms, p99=15.2ms
Preemption: 0
------------------ Summary --------------------
PASS
A.12 版本兼容矩阵
vLLM torch CUDA chunked prefill fp8 KV status
0.6.3 2.3.1 12.4 stable stable recommended
0.5.4 2.2.0 12.1 stable beta old but safe
0.4.3 2.1.2 12.1 experimental unsupported avoid
A.13 systemd 服务与日志轮转
# /etc/systemd/system/vllm.service
[Unit]
Description=vLLM inference server
After=docker.service
Requires=docker.service
[Service]
Type=oneshot
RemainAfterExit=yes
ExecStart=/usr/local/bin/start.sh
ExecStop=/usr/local/bin/healthcheck.sh
Restart=on-failure
RestartSec=10
[Install]
WantedBy=multi-user.target
# /etc/logrotate.d/vllm
/var/log/vllm.log {
daily
rotate 14
compress
delaycompress
missingok
notifempty
copytruncate
}
A.14 显存采样脚本
#!/usr/bin/env bash
# sample_gpu.sh —— 显存曲线采样,配合泄漏排查
LOG=${1:-/tmp/gpu_mem.csv}
echo "timestamp,mem_mb" > "$LOG"
while true; do
mem=$(nvidia-smi --query-gpu=memory.used --format=csv,noheader,nounits)
echo "$(date +%s),$mem" >> "$LOG"
sleep 60
done
A.15 并发阶梯统计
#!/usr/bin/env bash
# ladder.sh —— 并发阶梯压测,输出吞吐拐点
for c in 8 16 32 64 128 256; do
out=$(python -m vllm.bench_serving --model meta-llama/Meta-Llama-3.1-8B --max-concurrency $c --num-prompts 500 --request-rate 200 2>&1)
tp=$(echo "$out" | grep -oE "[0-9,.]+ requests/s" | head -1)
ttft=$(echo "$out" | grep -oE "p99=[0-9.]+ms" | head -1)
pre=$(echo "$out" | grep -iE "preemption" | head -1)
echo "conc=$c tp=$tp ttft=$ttft pre=$pre"
done
A.16 参数决策速查
Symptom Action
CUDA OOM at startup lower gpu-mem-util to 0.90, cut max-model-len
GPU 100% & throughput 0 bring max-num-seqs within KV pool
TTFT p99 > 1s enable chunked prefill, size 4096
concurrency stuck at ~30 halve max-model-len or kv-cache fp8
memory creeping > 1GB/day check prefix-caching & version, rollback
multi-GPU slow enable NCCL_IB, disable custom-all-reduce
A.17 CI 冒烟测试
# .gitlab-ci.yml
smoke:
stage: test
script:
- docker run -d --gpus all -p 8000:8000 vllm/vllm-openai:latest
/bin/sh -c "vllm serve meta-llama/Meta-Llama-3.1-8B
--gpu-memory-utilization 0.90 --max-num-seqs 64"
- sleep 60
- curl -sf http://localhost:8000/health
- python -m vllm.bench_serving --model meta-llama/Meta-Llama-3.1-8B
--max-concurrency 16 --num-prompts 50
A.18 bench_ladder.py
# bench_ladder.py —— 并发阶梯 + 逐档统计
import asyncio, aiohttp, time, statistics, sys
URL = "http://localhost:8000/v1/completions"
MODEL = "meta-llama/Meta-Llama-3.1-8B"
LEVELS = [8, 16, 32, 64, 128]
async def one(session, sem):
async with sem:
t0 = time.perf_counter()
async with session.post(URL, json={
"model": MODEL, "prompt": "Hello world",
"max_tokens": 64}) as r:
await r.json()
return time.perf_counter() - t0
async def level(session, conc, n=300):
sem = asyncio.Semaphore(conc)
t0 = time.perf_counter()
await asyncio.gather(*[one(session, sem) for _ in range(n)])
el = time.perf_counter() - t0
return n / el
async def main():
async with aiohttp.ClientSession() as s:
for c in LEVELS:
tp = await level(s, c)
print(f"conc={c:4d} throughput={tp:8.1f} req/s")
asyncio.run(main())
A.19 alert_preempt.sh
#!/usr/bin/env bash
# alert_preempt.sh —— 抢占超阈值即告警
LOG=${1:-/var/log/vllm.log}
THRESH=${2:-5}
pre=$(grep -c "preempt" "$LOG")
rate=$(echo "scale=2; $pre / 300" | bc)
if (( $(echo "$rate > $THRESH" | bc -l) )); then
echo "ALERT: preemption rate $rate/5min > $THRESH"
echo "action: reduce --max-num-seqs, check KV pool"
exit 1
fi
exit 0
A.20 k8s deployment
# deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: vllm
spec:
replicas: 1
template:
spec:
containers:
- name: vllm
image: vllm/vllm-openai:latest
args: ["serve", "meta-llama/Meta-Llama-3.1-8B",
"--max-model-len", "16384",
"--gpu-memory-utilization", "0.90",
"--max-num-seqs", "128",
"--enable-chunked-prefill"]
resources:
limits:
nvidia.com/gpu: 1
readinessProbe:
httpGet: { path: /health, port: 8000 }
A.21 KV pool estimation
GPU memory total: 40 GB (A100)
gpu-memory-utilization: 0.90
vLLM budget: 36 GB
model weights (fp16 8B): 16 GB
activation + overhead: 2 GB
KV pool budget: 18 GB
block size: 16 tokens
per-token KV (fp16): 0.5 KB * 2
KV pool tokens: 18 GB / 1 KB ~ 18M tokens
max-model-len: 16384
max concurrent (est): 18M / 16K ~ 1100
实际建议 max-num-seqs: 128(留 8 倍安全边际)
A.22 一键调优评估脚本(tune_check.sh)
#!/usr/bin/env bash
# tune_check.sh —— 调优前后一键对比,输出四指标
set -uo pipefail
MODEL=${MODEL:-meta-llama/Meta-Llama-3.1-8B}
URL=${URL:-http://localhost:8000}
LOG=${LOG:-/var/log/vllm.log}
run_bench() {
python -m vllm.bench_serving --model "$MODEL" --endpoint /v1/completions --max-concurrency 64 --num-prompts 500 --request-rate 50 --tokenizer "$MODEL" 2>&1
}
collect() {
local out
out=$(run_bench)
local tp ttft pre
tp=$(echo "$out" | grep -oE "[0-9,.]+ requests/s" | head -1)
ttft=$(echo "$out" | grep -oE "p99=[0-9.]+ms" | head -1)
pre=$(grep -c "preempt" "$LOG")
echo "throughput=$tp ttft_p99=$ttft preemption=$pre"
}
echo "BEFORE: $(collect)"
echo "AFTER : $(collect)"
跑两遍(调参前 / 调参后),看 throughput 升、ttft_p99 降、preemption 归零。
A.23 日志分析命令速查
# 抢占趋势(每分钟)
grep "preempt" /var/log/vllm.log | awk '{print $1}' | uniq -c
# KV 池初始化
grep -i "kv cache" /var/log/vllm.log
# 错误与异常
grep -iE "error|exception|traceback" /var/log/vllm.log | tail -20
# 活跃请求数曲线
grep -oE "running=[0-9]+" /var/log/vllm.log | tail -50 | sort | uniq -c
A.24 快速参考:调优一句话口诀
KV 池决定并发,max-num-seqs 别拍脑袋
长 prompt 阻塞短请求,就开 chunked prefill
显存不够用 fp8 KV,别硬扛
抢占风暴先降并发,再看 KV 池
升级版本必盯显存 48 小时
A.25 关键环境变量速查
VLLM_USE_PREFILL=False disable prefill prefetch for decode
NCCL_IB_DISABLE=0 enable InfiniBand for multi-GPU
NCCL_SHM_DISABLE=0 use shared memory
CUDA_VISIBLE_DEVICES=0,1 pin GPUs
VLLM_LOG_LEVEL=INFO log verbosity