
故事的开始:收到咨询“预热怎么搞?”
前不久收到咨询:“本地 vLLM 部署的模型,预热怎么做?首个 token 总是慢,有没有实战细节?”
我心想:“vLLM 不是已经够快了吗?” 结果一查日志,首轮请求还是触发了模型加载、CUDA 内核 JIT、KV Cache 申请,GPU 还在掉电待机状态…难怪要等。
于是我专门记录了优化全过程并“肝”了本文,分享:如何在本地 GPU 上用 vLLM 部署大模型,并把预热做到极致,让用户一上线就感受到“秒回”的丝滑。
首先,vLLM 是个啥?它适合跑哪些开源模型?
敲黑板: vLLM = 高性能推理引擎,核心是 PagedAttention + 连续批处理(continuous batching),原生支持 OpenAI 兼容接口。
吃苦耐劳的我进行了一番搜索并整理,目前社区里的模型热度排名(按公开版本常用度排序)如下:
- Qwen2.5 系列:7B/14B/32B/72B Instruct,中文/多语表现强,社区活跃度高
- Llama 3.1:8B/70B Instruct,生态插件/量化多
- Mixtral & Mistral:Mistral Nemo 12B、Mixtral 8x7B、8x22B(MoE)
- Yi 1.5:9B/34B Instruct,中文长文稳定
- GLM-4 9B / ChatGLM3/4 6B/9B:对话/工具调用成熟
- Gemma 2:9B/27B(指令版更好用)
- Phi-3.5:mini/medium,极致性价比
- 量化变体:AWQ、GPTQ、GGUF(注意 flash-attn/rope 支持)
模型选择指南:
| 模型 | 参数量 | 显存需求(fp16) | 中文能力 | 推理速度 | 适用场景 |
|---|---|---|---|---|---|
| Qwen2.5-Instruct | 7B/14B/32B/72B | 14/28/64/144 GB | 五星 | 快 | 通用对话、RAG |
| Llama 3.1 | 8B/70B | 16/140 GB | 三星 | 快 | 英文为主 |
| Yi-1.5 | 9B/34B | 18/68 GB | 五星 | 快 | 长文本(200k) |
| GLM-4 | 9B | 18 GB | 五星 | 中 | 工具调用 |
| Mistral-Nemo | 12B | 24 GB | 三星 | 快 | 代码生成 |
| Mixtral-8x7B | 47B(MoE) | 94 GB | 三星 | 中 | 复杂推理 |
以上排名和横评未必正确无误,如有出入,欢迎留言纠正!
本文记录的是使用单卡(RTX 3070 16GB 显存)、内存 64GB 跑 Qwen/Qwen2.5-7B-Instruct (fp16) 的实战过程,模型选择仅供参考,你可以根据需求选择模型。
为何选它? Qwen2.5-7B-Instruct 兼顾中文/多语体验、7B 体量适配 16GB 显存单卡,社区案例多、量化/部署资料丰富,易复制复现。
环境准备
硬件与软件要求
| 组件 | 最低要求 | 推荐配置 | 本文测试环境 |
|---|---|---|---|
| GPU | 8GB VRAM | 16GB+ VRAM | RTX 3070 16GB |
| CUDA | 11.8 | 12.1+ | 12.1 |
| Driver | 525.x | 535+ | 535.183 |
| Python | 3.9 | 3.10/3.11 | 3.10 |
| PyTorch | 2.1.0 | 2.2.0+ | 2.2.1+cu121 |
| RAM | 32GB | 64GB+ | 64GB |
创建虚拟环境
python -m venv vllm_env
source vllm_env/bin/activate # Linux/Mac
# vllm_env\Scripts\activate # Windows
安装核心依赖(自动匹配 CUDA 版本)
pip install torch==2.2.1 --index-url https://download.pytorch.org/whl/cu121
pip install “vllm>=0.5.0” “transformers>=4.40.0”
pip install “openai>=1.40.0” “huggingface_hub>=0.22.0”
pip install matplotlib pandas # 用于性能分析
可选:Flash Attention(显著加速)
pip install flash-attn --no-build-isolation
① 以为“启动 = 上线”,被「首 token」 上了一课
我信心满满地敲下:
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 --port 8000 \
--gpu-memory-utilization 0.9 \
--max-model-len 4096
(提示:若模型需从 Hugging Face 拉取,先 huggingface-cli login 或设置 HF_TOKEN;如 16GB 显存仍吃紧,可把 --gpu-memory-utilization 调低至 0.88 或把 --max-model-len 下调到 3072。)
端口通了,健康检查 OK,发第一条请求:
TTFT(首 token 延迟):5~6s
第二条请求:
TTFT:≈1s
差距一眼明了:冷启动伤不起。
② 翻日志才知道,预热要触发的东西太多了
冷启动的隐形成本:
- 分词器懒加载:第一条中文/符号混合 prompt 最慢
- CUDA kernel JIT:flash-attn、rope、rmsnorm 等都要编译
- KV Cache 申请:长 prompt 会触发大页分配
- 调度器初始化:continuous batching 的批次合并要先跑一遍
- GPU 掉电:没开 nvidia-persistenced,GPU 频率慢慢爬
目标很明确:把这些成本提前花掉。
③ 设计预热策略,覆盖“长短胖瘦”所有姿势
我列了一张预热覆盖表:
| 维度 | 要覆盖什么 | 目的 |
|---|---|---|
| Prompt 长度 | 32 / 256 / 1024+ | 触发分词缓存、KV 大页申请 |
| 输出长度 | 32 / 128 | 让 decode 内核 JIT 完整 |
| 并发 | 4 / 8 / 16 | 让调度器、连续批次稳定 |
| 语料类型 | 中/英/符号混合 | 填满分词 cache,避免首条中文慢 |
| 特殊路径 | 流式 / 非流式 | SSE/HTTP chunking 都走一遍 |
预热脚本要“一次打穿”这些组合,不能只发一条短 prompt。
李牛子前辈说:“Talk is cheap. Show me the code.”

好吧!
④ 上代码:预热脚本
# warmup_vllm.py
import asyncio, time, sys
from openai import AsyncOpenAI
client = AsyncOpenAI(
base_url="http://127.0.0.1:8000/v1",
api_key="not-used",
timeout=30,
)
base_prompts = [
"用 3 点概括 Transformer 的核心思想。",
"Write a concise intro to CUDA graph capture for attention kernels.",
"给出一篇 1200 字的综述:vLLM 的调度、KV Cache 分页与连续批次。",
"Symbols: $$$ *** ### 混合一下 tokenizer 缓存。",
]
length_pairs = [(32, 64), (256, 64), (1024, 128)] # (target_prompt_len, max_tokens)
def make_prompt_with_length(base_prompt, target_len):
"""扩展 prompt 到目标长度(按字符数估算)"""
if len(base_prompt) >= target_len:
return base_prompt
# 构造填充内容,让 prompt 达到目标长度
padding_text = "请详细展开说明,包括具体的技术细节、实现原理、优化方法、性能指标、应用场景。"
repeat_count = (target_len - len(base_prompt)) // len(padding_text) + 1
padding = (padding_text * repeat_count)[:target_len - len(base_prompt)]
return base_prompt + padding
async def send(prompt, max_tokens):
try:
t0 = time.time()
resp = await client.chat.completions.create(
model="Qwen/Qwen2.5-7B-Instruct",
messages=[{"role": "user", "content": prompt}],
max_tokens=max_tokens,
temperature=0.2,
stream=False,
)
dt = time.time() - t0
preview = resp.choices[0].message.content[:60].replace("\n", " ")
return dt, preview
except Exception as e:
print(f"预热请求失败: {e}", file=sys.stderr)
return 0, f"ERROR: {str(e)[:50]}"
async def main():
print("开始预热 vLLM 服务...")
tasks = []
# 生成不同长度的 prompt 组合
for base_prompt in base_prompts:
for prompt_len, max_tok in length_pairs:
# 根据目标长度扩展 prompt
extended_prompt = make_prompt_with_length(base_prompt, prompt_len)
print(f" 准备长度 ~{len(extended_prompt)} 的 prompt,max_tokens={max_tok}")
# 并发 4 个相同请求,测试并发处理能力
for _ in range(4):
tasks.append(asyncio.create_task(send(extended_prompt, max_tok)))
# 等待所有任务完成
results = await asyncio.gather(*tasks, return_exceptions=True)
# 统计结果
success_count = sum(1 for r in results if isinstance(r, tuple) and r[0] > 0)
error_count = len(results) - success_count
print(f"\n预热完成!成功: {success_count}, 失败: {error_count}")
# 显示部分结果
for i, result in enumerate(results[:10]): # 只显示前10个
if isinstance(result, tuple):
dt, preview = result
if dt > 0:
print(f"[{i:02d}] {dt:.3f}s {preview!r}")
else:
print(f"[{i:02d}] 异常: {result}")
if __name__ == "__main__":
asyncio.run(main())
两轮跑下来,TTFT 从 5.x s 掉到 0.7~1.x s,延迟波动也小了很多。显存紧张或担心 OOM 时,把每个 prompt 的并发从 4 改成 2。
⑤ 把预热“产品化”——启动即触发
启动后自动预热
- 用 systemd 管 vLLM,再在
ExecStartPost调用 warmup:
[Service]
ExecStart=/usr/bin/python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 --port 8000 \
--gpu-memory-utilization 0.9 \
--max-model-len 4096
# 等待服务就绪后再执行预热
ExecStartPost=/bin/bash -c 'for i in {1..30}; do \
curl -s http://127.0.0.1:8000/v1/models && break || sleep 2; \
done && /usr/bin/python /opt/warmup_vllm.py'
Restart=on-failure
RestartSec=10
- 或者在 CI/CD 的部署脚本里:
# 启动服务后等待就绪
echo "等待 vLLM 服务启动..."
for i in {1..30}; do
if curl -s http://127.0.0.1:8000/v1/models > /dev/null; then
echo "服务已就绪,开始预热"
break
fi
sleep 2
done
# 执行预热脚本
python warmup_vllm.py || (echo "warmup failed" && exit 1)
定时保活
长时间无流量,GPU 可能降频、cache 失效。用 cron 每 15 分钟打一次轻量请求:
*/15 * * * * /usr/bin/python /opt/warmup_ping.py >> /var/log/vllm_warm.log 2>&1
warmup_ping.py 可以只发一条 32-token 的短请求,不影响显存。
⑥ vLLM 参数也要配合预热,否则可能白忙
--gpu-memory-utilization 0.9:留 10% 余量,避免预热时 OOM--max-model-len 4096/8192:过小长 prompt 会失败,过大会撑爆显存,预热前先算好显存占用--enforce-eager:默认 False,必要时可打开便于调试内核编译--max-num-batched-tokens:在高并发场景适当放大,避免预热覆盖不到大 batch- 多卡:
--tensor-parallel-size N,预热脚本要真正打到所有 GPU,别只打到 rank0 - 低显存(8/12GB)量化方案:
# 12GB 显存:使用 AWQ 4bit 量化 python -m vllm.entrypoints.openai.api_server \ --model TheBloke/Qwen2.5-7B-Instruct-AWQ \ --quantization awq \ --gpu-memory-utilization 0.85 \ --max-model-len 2048 # 8GB 显存:使用 GPTQ 4bit 量化 python -m vllm.entrypoints.openai.api_server \ --model TheBloke/Qwen2.5-7B-Instruct-GPTQ \ --quantization gptq \ --gpu-memory-utilization 0.8 \ --max-model-len 1024
⑦ 观测验证:数据说话才有底气
对比表(RTX 3070 16GB / Qwen2.5-7B-Instruct / fp16):
| 指标 | 冷启动 | 预热后 |
|---|---|---|
| TTFT(中等 prompt) | 5.4s | 1.1s |
| TTFT(长 prompt 1k) | 6.9s | 1.7s |
| P95 波动 | 2.6s | 0.7s |
| GPU 利用率爬升时间 | 9s | 2.3s |
注:上述为本机实测,若硬件/驱动/模型不同,数值仅供参考,重点看“首轮 vs 次轮”的下降幅度。
监控建议:
nvidia-smi dmon -s pucvmet看功耗/显存/频率- vLLM 日志中的
prefill,decodelatency(tail -f观察) - 简单压测:
hey -n 200 -c 16 http://127.0.0.1:8000/v1/chat/completions
架构长啥样?一图概括
快速避坑清单
# 1) GPU 持久化,避免掉电重启
sudo nvidia-persistenced
# 2) 先确认显存足够,再设 max-model-len
python - <<'PY'
# Qwen2.5-7B 显存需求计算
model_params_gb = 7 * 2 # 7B params * 2 bytes (fp16) = 14GB
# KV Cache 计算
hidden=4096; layers=32
max_len=4096; dtype_bytes=2 # fp16
# 每 token KV = hidden * 2 (K+V) * layers * dtype
kv_per_token = hidden * 2 * layers * dtype_bytes
kv_cache_gb = kv_per_token * max_len / 1024 / 1024 / 1024
# 总显存需求(预估)
total_gb = model_params_gb + kv_cache_gb + 0.5 # 0.5GB for activation
print(f"模型权重: {model_params_gb:.1f} GB")
print(f"KV Cache (max_len={max_len}): {kv_cache_gb:.1f} GB")
print(f"激活值和其他: ~0.5 GB")
print(f"总需求: ~{total_gb:.1f} GB")
print(f"\n建议配置:")
print(f"16GB 显存: --max-model-len 4096 --gpu-memory-utilization 0.9")
print(f"24GB 显存: --max-model-len 8192 --gpu-memory-utilization 0.9")
print(f"12GB 显存: 使用量化版本 (AWQ/GPTQ 4bit)")
PY
# 3) 预热脚本务必覆盖长 prompt + 并发
# 4) 如果开了 LoRA / speculative,预热也要打到对应路径
# 5) 观测 TTFT 改善:跑两轮 warmup,再压测
一键可用的示例配置
启动命令:
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-7B-Instruct \
--host 0.0.0.0 --port 8000 \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.9 \
--max-model-len 4096 \
--max-num-batched-tokens 8192
预热脚本调用:
python warmup_vllm.py
python warmup_vllm.py # 再跑一遍看 TTFT 差异
简易压测:
hey -n 200 -c 16 \
-m POST \
-H "Content-Type: application/json" \
-d '{"model":"Qwen/Qwen2.5-7B-Instruct","messages":[{"role":"user","content":"hello"}]}' \
http://127.0.0.1:8000/v1/chat/completions
此番折腾的收获
踩过的坑:
- 忘开 GPU 持久化,冷启动功耗/频率爬升慢
- 只预热短 prompt,结果第一条长对话仍然 3s+
- 多卡只打到 rank0,其他卡的内核没 JIT
--max-model-len估算不足,长 prompt 直接 OOM
值得复用的经验:
- 预热要覆盖“长短胖瘦”:长度、并发、语料类型、流式路径都要走一遍
- 预热应产品化:部署脚本/cron 内置,不靠“有人想起来就跑一下”
- 参数先算后设:KV Cache 是按 max_len 预留的,别凭感觉
- 观测闭环:TTFT、P95、GPU 频率、日志都要看,别只拍脑袋
写在最后:让用户一上来就觉得“真快”
vLLM 已经帮我们解决了大模型推理的绝大部分性能问题,预热则是“临门一脚”:把所有冷启动成本前置、平滑掉波动,让体验从第一条请求就拉满。
把这篇攻略照着做,并举一反三地运用到你的工程中,让用户从「等半天」变成「怎么这么快」。
1284

被折叠的 条评论
为什么被折叠?



