大模型 demo 跑出来了,老板说"把它部署上线"。然后你发现每秒只能处理 3 个请求,延迟 8 秒——这就是很多人从「跑通模型」到「生产上线」最难受的一道坎。
vLLM 是当前解决这个问题的首选方案。它把 Qwen2.5-7B 的推理吞吐从 3 req/s 拉到 50+ req/s——还不需要改模型一行代码。
这篇文章用一套完整的部署链路——模型启动、API 封装、Docker 打包、压测验证——带你从零到上线。
1. vLLM 到底做了什么
传统的 Transformer 推理有几处严重的浪费:
- 显存碎片化:每个请求分配固定大小的 KV Cache,不同请求长度不同导致大量空闲
- 串行批处理:等一个 batch 里最长的那个请求算完,短请求的 GPU 计算单元一直在空转
- 内存拷贝:每次 forward 都在 CPU/GPU 之间搬数据
vLLM 的核心改进——PagedAttention——把 KV Cache 按「页」管理(类似操作系统虚拟内存)。空闲页被回收分配给新请求,显存利用率从 30% 拉到 90%+。
直观对比:同一张 A100,跑 Qwen2.5-7B:
| 方案 | 吞吐 (req/s) | 延迟 P50 | 延迟 P99 | 显存占用 |
|---|---|---|---|---|
| HuggingFace 原生 | 3.2 | 2.1s | 8.3s | 18 GB |
| vLLM (默认) | 28.7 | 0.4s | 1.8s | 16 GB |
| vLLM (prefix caching) | 52.3 | 0.2s | 0.9s | 14 GB |
2. 安装与环境
# vLLM 需要 CUDA 11.8+,推荐在 Linux 上安装 pip install vllm # 可选:FlashInfer 后端(比默认 FlashAttention 更快) pip install flashinfer -i https://flashinfer.ai/whl/cu124验证安装:
from vllm import LLM # 快速测试 llm = LLM(model="Qwen/Qwen2.5-1.5B-Instruct") outputs = llm.generate(["用一句话介绍北京"]) print(outputs[0].outputs[0].text)3. 离线批量推理
先看最简单的——不用起服务,直接批量跑推理。
# batch_inference.py — 离线批量推理 from vllm import LLM, SamplingParams # ── 模型加载 ── llm = LLM( model="Qwen/Qwen2.5-7B-Instruct", dtype="float16", # 推理用 fp16 足够 max_model_len=4096, # 限制最大长度以节省显存 gpu_memory_utilization=0.90, # 显存利用率(留一点给系统) tensor_parallel_size=1, # 单卡为 1,多卡设为 GPU 数量 trust_remote_code=True, enforce_eager=False, # False = 使用 CUDA Graph 加速 ) # ── 采样参数 ── sampling_params = SamplingParams( temperature=0.7, top_p=0.9, max_tokens=512, stop=["<|im_end|>", "<|endoftext|>"], repetition_penalty=1.05, ) # ── 批量推理 ── prompts = [ "<|im_start|>user\n解释一下什么是RESTful API<|im_end|>\n<|im_start|>assistant\n", "<|im_start|>user\n写一段Python快速排序<|im_end|>\n<|im_start|>assistant\n", "<|im_start|>user\n什么是Docker容器化<|im_end|>\n<|im_start|>assistant\n", ] outputs = llm.generate(prompts, sampling_params) for prompt, output in zip(prompts, outputs): generated = output.outputs[0].text token_count = len(output.outputs[0].token_ids) print(f"生成长度: {token_count} tokens") print(f"内容: {generated[:200]}...\n")4. OpenAI 兼容 API 服务
生产环境的核心需求:HTTP 接口、流式输出、多客户端并发。vLLM 自带 OpenAI 兼容服务器。
# 启动 OpenAI 兼容 API 服务 python -m vllm.entrypoints.openai.api_server \ --model Qwen/Qwen2.5-7B-Instruct \ --dtype float16 \ --max-model-len 4096 \ --gpu-memory-utilization 0.90 \ --port 8000 \ --host 0.0.0.0 \ --enable-prefix-caching \ --max-num-seqs 256 \ --max-num-batched-tokens 8192关键参数: ---enable-prefix-caching:缓存系统 prompt 的 KV Cache,多轮对话场景大幅提速 ---max-num-seqs:同时处理的最大请求数(并行度上限) ---max-num-batched-tokens:单次 forward 的最大 token 数
客户端调用——和 OpenAI SDK 完全兼容:
# client.py — 用 OpenAI SDK 调 vLLM from openai import OpenAI client = OpenAI( base_url="http://localhost:8000/v1", api_key="not-needed", # vLLM 不需要 API key ) # 普通对话 response = client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[ {"role": "system", "content": "你是 AI 部署专家"}, {"role": "user", "content": "解释 vLLM 的 PagedAttention 原理"}, ], temperature=0.7, max_tokens=512, ) print(response.choices[0].message.content) # 流式输出 stream = client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=[{"role": "user", "content": "写一首关于AI的诗"}], stream=True, ) for chunk in stream: if chunk.choices[0].delta.content: print(chunk.choices[0].delta.content, end="", flush=True)5. FastAPI 服务封装
生产环境通常还要加一层自己的 FastAPI 服务——鉴权、限流、日志、prompt 模板管理、结果缓存。
# api_server.py — FastAPI 生产级封装 from fastapi import FastAPI, HTTPException, Depends from fastapi.responses import StreamingResponse from fastapi.middleware.cors import CORSMiddleware from pydantic import BaseModel, Field from openai import AsyncOpenAI import asyncio import time import logging app = FastAPI(title="vLLM Chat API", version="1.0.0") app.add_middleware(CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]) vllm_client = AsyncOpenAI(base_url="http://localhost:8000/v1", api_key="not-needed") logger = logging.getLogger("vllm_api") # ── 请求模型 ── class ChatRequest(BaseModel): messages: list[dict] = Field(..., description="对话消息列表") temperature: float = Field(0.7, ge=0.0, le=2.0) max_tokens: int = Field(512, ge=1, le=4096) stream: bool = Field(False) class ChatResponse(BaseModel): content: str model: str tokens_used: int time_elapsed: float # ── 简易 token 鉴权 ── async def verify_token(x_api_key: str | None = None): # 生产环境换 JWT 或 OAuth2 if x_api_key and x_api_key == "your-secret-token": return True # raise HTTPException(401, "Invalid token") return True # 示例中放行 # ── 非流式接口 ── @app.post("/v1/chat", response_model=ChatResponse) async def chat(req: ChatRequest, _=Depends(verify_token)): t0 = time.time() try: response = await vllm_client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=req.messages, temperature=req.temperature, max_tokens=req.max_tokens, timeout=30, ) except asyncio.TimeoutError: raise HTTPException(504, "模型推理超时") except Exception as e: logger.error(f"vLLM error: {e}") raise HTTPException(500, f"推理服务异常: {e}") elapsed = time.time() - t0 choice = response.choices[0] logger.info(f"chat done: {choice.finish_reason}, tokens={response.usage.total_tokens}, time={elapsed:.2f}s") return ChatResponse( content=choice.message.content, model=response.model, tokens_used=response.usage.total_tokens, time_elapsed=round(elapsed, 2), ) # ── 流式接口 ── @app.post("/v1/chat/stream") async def chat_stream(req: ChatRequest, _=Depends(verify_token)): async def event_generator(): try: stream = await vllm_client.chat.completions.create( model="Qwen/Qwen2.5-7B-Instruct", messages=req.messages, temperature=req.temperature, max_tokens=req.max_tokens, stream=True, ) async for chunk in stream: if chunk.choices[0].delta.content: yield f"data: {chunk.choices[0].delta.content}\n\n" yield "data: [DONE]\n\n" except Exception as e: yield f"data: [ERROR] {e}\n\n" return StreamingResponse(event_generator(), media_type="text/event-stream") # 启动: uvicorn api_server:app --host 0.0.0.0 --port 80806. Docker 容器化部署
# Dockerfile FROM nvidia/cuda:12.4.0-runtime-ubuntu22.04 RUN apt-get update && apt-get install -y python3.11 python3-pip && \ ln -s /usr/bin/python3.11 /usr/bin/python WORKDIR /app RUN pip install vllm fastapi uvicorn openai -i https://pypi.tuna.tsinghua.edu.cn/simple COPY api_server.py . # 预下载模型(构建时下载,避免启动等待) RUN huggingface-cli download Qwen/Qwen2.5-7B-Instruct --local-dir /models/Qwen2.5-7B EXPOSE 8080 CMD ["sh", "-c", "python -m vllm.entrypoints.openai.api_server \ --model /models/Qwen2.5-7B \ --port 8000 --host 0.0.0.0 & \ uvicorn api_server:app --host 0.0.0.0 --port 8080"]# docker-compose.yml version: "3.8" services: vllm-server: build: . ports: - "8080:8080" environment: - CUDA_VISIBLE_DEVICES=0 deploy: resources: reservations: devices: - driver: nvidia count: 1 capabilities: [gpu] volumes: - ./models:/models restart: unless-stopped# 构建 & 启动 docker compose build docker compose up -d7. 压力测试
# 用 vegeta 做压测 echo 'POST http://localhost:8080/v1/chat' | \ vegeta attack -body test_payload.json -rate=50/s -duration=60s | \ vegeta report # 典型输出(A100,Qwen2.5-7B,prefix caching 开启): # Requests [total, rate, throughput] 3000, 50.00, 48.72 # Latencies [mean, 50, 95, 99, max] 412ms, 389ms, 621ms, 1.1s, 2.4s # Success Rate [ratio] 100.00%// test_payload.json { "messages": [ {"role": "user", "content": "简述微服务和单体架构的区别"} ], "temperature": 0.7, "max_tokens": 256 }8. 生产 Checklist
上线前对照这份清单逐项确认:
□ GPU 驱动 >= 525.60.13(nvidia-smi 确认) □ CUDA >= 11.8(nvcc --version 确认) □ vLLM >= 0.6.0(pip show vllm 确认) □ gpu_memory_utilization 设为 0.85-0.90(留余量给 CUDA context) □ 开启 --enable-prefix-caching(多轮对话场景必须) □ 配置 systemd supervisor 自动重启(vLLM 偶有 CUDA OOM 崩溃) □ 加 health check endpoint(GET /health → {"status": "ok"}) □ 接入 Prometheus metrics(vLLM 自带 /metrics) □ 单 GPU 故障时自动切换(如果有双卡,用 nginx upstream 做 failover)踩坑记录
- Docker 里 CUDA 版本要对齐— 宿主机
nvidia-smi显示的 CUDA 版本和 Docker 镜像的cuda:x.x.x-runtime必须一致。不一致的话启动时直接 Segmentation Fault。 max_model_len设太大反而慢— 每个请求都会预留这个长度的 KV Cache 空间。如果你实际对话不超过 2048 token,设 4096 就是在浪费显存。- 内存泄漏— vLLM 0.5.x 以前有已知的内存泄漏 bug(issue #4712),长时运行显存缓慢增长。0.6.0 以上基本修了,但生产环境建议每周凌晨自动重启。
金句
"大模型部署不是把模型跑起来就完了——是从 3 req/s 到 50 req/s 的工程化跨越。"
如果你也在做模型部署,或者遇到了延迟/吞吐/显存的问题,评论区说说你的模型大小和 GPU 配置——我帮你算算预期吞吐。