人工智能实战:大模型接口并发低、GPU利用率上不去?基于 vLLM 重构推理服务的完整工程方案
一、问题场景:模型能跑,但一压测就露馅
最近在做一个内部知识库问答系统,模型本地推理已经跑通,FastAPI 接口也能正常返回结果。
一开始我以为事情差不多结束了,结果一做压测,问题马上暴露:
text
1. 单用户访问正常
2. 5个用户并发开始明显排队
3. 10个用户并发时接口超时
4. GPU显存占了很多,但GPU利用率只有20%~35%
5. 请求耗时波动非常大,有时2秒,有时十几秒
当时服务架构大概是这样:
text
用户请求
↓
FastAPI
↓
transformers generate
↓
GPU推理
↓
返回结果
代码看起来没有明显问题,模型也只在启动时加载了一次:
python
model = AutoModelForCausalLM.from_pretrained(...)
tokenizer = AutoTokenizer.from_pretrained(...)
但实际表现非常差。
这就是大模型系统部署里一个很典型的坑:
text
模型能推理,不代表推理服务能承载并发。
二、先复现问题:transformers 原生推理接口
为了方便复现,我们先写一个最常见的 FastAPI + transformers 推理服务。
1. 安装依赖
bash
pip install fastapi uvicorn torch transformers accelerate
2. 项目结构
text
llm-transformers-demo/
├── app.py
└── requirements.txt
3. 编写 app.py
python
import time
import torch
from fastapi import FastAPI
from pydantic import BaseModel, Field
from transformers import AutoTokenizer, AutoModelForCausalLM
app = FastAPI(title="Transformers LLM Service")
MODEL_NAME = "Qwen/Qwen2.5-0.5B-Instruct"
tokenizer = None
model = None
class ChatRequest(BaseModel):
prompt: str = Field(..., min_length=1, max_length=2000)
max_new_tokens: int = Field(default=128, ge=1, le=512)
@app.on_event("startup")
def load_model():
global tokenizer, model
print("开始加载模型...")
tokenizer = AutoTokenizer.from_pretrained(
MODEL_NAME,
trust_remote_code=True
)
model = AutoModelForCausalLM.from_pretrained(
MODEL_NAME,
torch_dtype=torch.float16 if torch.cuda.is_available() else torch.float32,
device_map="auto",
trust_remote_code=True
)
model.eval()
print("模型加载完成")
@app.post("/chat")
def chat(req: ChatRequest):
start_time = time.time()
inputs = tokenizer(
req.prompt,
return_tensors="pt",
truncation=True,
max_length=2048
).to(model.device)
with torch.inference_mode():
outputs = model.generate(
**inputs,
max_new_tokens=req.max_new_tokens,
do_sample=True,
temperature=0.7,
top_p=0.9
)
answer = tokenizer.decode(outputs[0], skip_special_tokens=True)
cost = round(time.time() - start_time, 3)
return {
"answer": answer,
"cost_seconds": cost
}
@app.get("/health")
def health():
return {"status": "ok"}
4. 启动服务
bash
uvicorn app:app --host 0.0.0.0 --port 8000
5. 单次请求测试
bash
curl -X POST "http://127.0.0.1:8000/chat" \
-H "Content-Type: application/json" \
-d '{
"prompt": "请用通俗语言解释一下Transformer的自注意力机制",
"max_new_tokens": 128
}'
单请求一般能正常返回。
但问题不是单请求,而是并发。
三、压测复现:为什么单测正常,并发就崩?
1. 安装压测工具
bash
pip install locust
2. 编写 locustfile.py
python
from locust import HttpUser, task, between
class LLMUser(HttpUser):
wait_time = between(1, 2)
@task
def chat(self):
self.client.post(
"/chat",
json={
"prompt": "请解释一下深度学习模型推理为什么需要GPU加速",
"max_new_tokens": 128
},
timeout=60
)
3. 启动压测
bash
locust -f locustfile.py --host=http://127.0.0.1:8000
在浏览器打开:
text
http://127.0.0.1:8089
设置:
text
Number of users: 20
Spawn rate: 2
四、观察到的问题
压测后你会看到类似现象:
text
1. 平均响应时间越来越高
2. P95 延迟明显上升
3. 请求出现排队
4. GPU 利用率并没有打满
5. 显存占用不低,但吞吐上不去
可以用下面命令观察 GPU:
bash
nvidia-smi -l 1
你可能会看到:
text
GPU Memory Usage: 很高
GPU Utilization: 20%~40%
这说明:
text
显存占了,但算力没吃满。
五、原因分析:瓶颈不在模型,而在推理调度
很多人看到接口慢,第一反应是:
text
是不是模型太大?
是不是GPU太差?
是不是代码没优化?
这些都有可能,但在这个场景里,真正核心的问题是:
text
transformers 的 generate 更偏单请求推理,不是高并发推理服务框架。
六、transformers 推理的核心问题
1. 请求之间缺少统一调度
多个请求进来时,服务层看到的是:
text
请求A → generate
请求B → generate
请求C → generate
它不会自动把多个请求组织成高效 batch。
2. GPU 没有被充分喂满
GPU 擅长的是并行矩阵计算。
但如果每次只处理一个请求,实际执行类似这样:
text
小矩阵计算
小矩阵计算
小矩阵计算
这会导致大量 GPU 计算资源空闲。
3. KV Cache 管理效率不高
大模型推理不是一次性算完,而是逐 token 生成。
每生成一个 token,都依赖前面 token 的 Key / Value 缓存。
也就是说:
text
输入越长,KV Cache 越大
输出越长,KV Cache 越大
并发越高,KV Cache 越难管理
如果没有高效的 KV Cache 管理机制,很容易出现:
text
显存碎片
吞吐下降
延迟抖动
七、解决思路:用 vLLM 替代 transformers 做推理服务
vLLM 解决的不是"模型能不能跑",而是:
text
大模型如何高吞吐服务化。
它的核心优势主要有三个:
text
1. Continuous Batching:连续批处理
2. PagedAttention:更高效的 KV Cache 管理
3. OpenAI Compatible Server:可直接对外提供接口
八、vLLM 架构理解
1. 原始 transformers 模式
text
请求1 → 单独推理
请求2 → 单独推理
请求3 → 单独推理
2. vLLM 模式
text
请求1
请求2
请求3
↓
调度器统一管理
↓
动态组成 batch
↓
GPU 高效执行
↓
分别返回结果
它的关键不是简单 batch,而是连续 batch。
普通 batch 的问题是:
text
必须等一批请求都完成,才能开始下一批。
vLLM 的 Continuous Batching 可以在生成过程中动态加入新请求。
这对线上服务非常重要。
九、使用 vLLM 启动 OpenAI 兼容服务
1. 安装 vLLM
bash
pip install vllm
如果你是 CUDA 环境,建议先确认:
bash
nvidia-smi
python -c "import torch; print(torch.cuda.is_available())"
2. 启动 vLLM 服务
bash
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-0.5B-Instruct \
--host 0.0.0.0 \
--port 8001 \
--trust-remote-code \
--gpu-memory-utilization 0.85 \
--max-model-len 2048
几个参数说明:
text
--model:模型名称或本地模型路径
--trust-remote-code:部分国产模型需要
--gpu-memory-utilization:限制显存使用比例
--max-model-len:限制最大上下文长度
十、验证 vLLM 服务
1. 健康检查
bash
curl http://127.0.0.1:8001/health
2. 请求 completions 接口
bash
curl http://127.0.0.1:8001/v1/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-0.5B-Instruct",
"prompt": "请解释一下什么是大模型推理服务",
"max_tokens": 128,
"temperature": 0.7
}'
3. 请求 chat completions 接口
bash
curl http://127.0.0.1:8001/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "Qwen/Qwen2.5-0.5B-Instruct",
"messages": [
{
"role": "user",
"content": "请用工程师能听懂的话解释vLLM为什么比transformers更适合部署"
}
],
"max_tokens": 128,
"temperature": 0.7
}'
十一、工程封装:FastAPI 调用 vLLM 服务
生产环境里,我不建议业务系统直接写死 vLLM 接口。
更推荐做一层业务网关:
text
业务系统
↓
AI Gateway
↓
vLLM Server
这样后续可以统一做:
text
鉴权
限流
日志
模型路由
灰度发布
异常兜底
1. 安装依赖
bash
pip install fastapi uvicorn httpx pydantic
2. 项目结构
text
llm-vllm-gateway/
├── app.py
├── schemas.py
├── llm_client.py
└── requirements.txt
3. schemas.py
python
from pydantic import BaseModel, Field
from typing import Optional
class ChatRequest(BaseModel):
prompt: str = Field(..., min_length=1, max_length=2000)
max_tokens: int = Field(default=128, ge=1, le=512)
temperature: float = Field(default=0.7, ge=0.0, le=2.0)
class ChatResponse(BaseModel):
answer: str
model: str
cost_ms: int
4. llm_client.py
python
import time
import httpx
class VLLMClient:
def __init__(self, base_url: str, model: str, timeout: int = 60):
self.base_url = base_url.rstrip("/")
self.model = model
self.timeout = timeout
async def chat(self, prompt: str, max_tokens: int, temperature: float):
start = time.time()
url = f"{self.base_url}/v1/chat/completions"
payload = {
"model": self.model,
"messages": [
{
"role": "user",
"content": prompt
}
],
"max_tokens": max_tokens,
"temperature": temperature
}
async with httpx.AsyncClient(timeout=self.timeout) as client:
resp = await client.post(url, json=payload)
resp.raise_for_status()
data = resp.json()
answer = data["choices"][0]["message"]["content"]
cost_ms = int((time.time() - start) * 1000)
return {
"answer": answer,
"model": self.model,
"cost_ms": cost_ms
}
5. app.py
python
from fastapi import FastAPI, HTTPException
from schemas import ChatRequest, ChatResponse
from llm_client import VLLMClient
app = FastAPI(title="LLM Gateway With vLLM")
client = VLLMClient(
base_url="http://127.0.0.1:8001",
model="Qwen/Qwen2.5-0.5B-Instruct",
timeout=60
)
@app.post("/chat", response_model=ChatResponse)
async def chat(req: ChatRequest):
try:
result = await client.chat(
prompt=req.prompt,
max_tokens=req.max_tokens,
temperature=req.temperature
)
return ChatResponse(**result)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"LLM request failed: {str(e)}"
)
@app.get("/health")
def health():
return {"status": "ok"}
6. 启动网关
bash
uvicorn app:app --host 0.0.0.0 --port 8000
7. 测试网关
bash
curl -X POST "http://127.0.0.1:8000/chat" \
-H "Content-Type: application/json" \
-d '{
"prompt": "解释一下vLLM中的Continuous Batching",
"max_tokens": 128,
"temperature": 0.7
}'
十二、压测对比:transformers vs vLLM
为了公平对比,保持以下条件一致:
text
同一台机器
同一个模型
同样的 prompt
同样 max_tokens
同样并发用户数
1. vLLM 压测脚本
python
from locust import HttpUser, task, between
class VLLMUser(HttpUser):
wait_time = between(1, 2)
@task
def chat(self):
self.client.post(
"/chat",
json={
"prompt": "请解释大模型推理服务中的KV Cache是什么",
"max_tokens": 128,
"temperature": 0.7
},
timeout=60
)
2. 建议记录的指标
text
平均响应时间
P95延迟
P99延迟
QPS
失败率
GPU利用率
显存占用
3. 结果示例
text
transformers:
平均响应:4800ms
P95:12000ms
QPS:2.1
失败率:8%
vLLM:
平均响应:1200ms
P95:2800ms
QPS:16.8
失败率:0%
注意:
text
具体数据会受显卡、模型大小、prompt长度影响。
这里重点不是绝对数值,而是优化方向。
十三、踩坑记录:这些问题我都遇到过
坑 1:vLLM 启动直接 OOM
启动时报错:
text
CUDA out of memory
常见原因:
text
1. max-model-len 设置过大
2. gpu-memory-utilization 设置太高
3. 模型本身超过显存能力
解决方式:
bash
python -m vllm.entrypoints.openai.api_server \
--model Qwen/Qwen2.5-0.5B-Instruct \
--gpu-memory-utilization 0.75 \
--max-model-len 2048
坑 2:长 prompt 拖慢整体吞吐
vLLM 可以动态 batching,但并不代表无限制输入。
如果某些请求 prompt 特别长,会导致:
text
1. batch 内部 padding 增加
2. KV Cache 占用增加
3. 整体吞吐下降
所以接口层必须限制:
python
prompt: str = Field(..., max_length=2000)
坑 3:业务接口没有超时控制
错误写法:
python
resp = await client.post(url, json=payload)
推荐:
python
async with httpx.AsyncClient(timeout=60) as client:
resp = await client.post(url, json=payload)
否则 vLLM 服务异常时,业务接口会被拖死。
坑 4:把 vLLM 服务和业务服务混在一个进程
不建议这样做:
text
FastAPI + vLLM 同进程
更推荐:
text
FastAPI网关进程
vLLM推理进程
原因:
text
1. 职责清晰
2. 方便独立重启
3. 方便单独扩容
4. 方便替换模型服务
十四、注意事项
1. 不要盲目追求最大显存利用率
很多人会设置:
bash
--gpu-memory-utilization 0.95
这看起来很爽,但线上很危险。
建议从:
text
0.75 ~ 0.85
开始调。
2. max_model_len 不要无脑拉满
上下文越长:
text
KV Cache越大
显存压力越高
吞吐越低
内部问答系统通常可以先设:
text
2048 或 4096
3. 网关层一定要做参数校验
至少限制:
text
prompt长度
max_tokens
temperature范围
请求超时
十五、适合收藏的部署步骤总结
text
1. 先用 transformers 跑通基础推理
2. 使用 locust 做并发压测
3. 观察 GPU 利用率和 P95 延迟
4. 如果 GPU 利用率低且并发差,切换 vLLM
5. 使用 vLLM OpenAI API Server 部署模型
6. 使用 FastAPI 做业务网关
7. 网关层增加参数校验和超时控制
8. 再次压测对比 QPS、P95、失败率
9. 根据显存情况调整 max-model-len
10. 根据稳定性调整 gpu-memory-utilization
十六、避坑清单
text
不要用 transformers 硬扛高并发
不要让业务直接裸调模型服务
不要无限制开放 max_tokens
不要把 prompt 长度放开
不要把 gpu-memory-utilization 设置过高
不要忽略 P95 / P99,只看平均耗时
不要把 vLLM 和业务逻辑塞进一个进程
不要没有超时、没有日志、没有失败兜底
十七、最终工程结论
这次优化给我的最大感受是:
text
大模型部署不是把 generate 包成接口就结束了。
真正的推理服务要考虑:
text
请求调度
显存管理
KV Cache
并发吞吐
超时控制
业务网关
监控压测
transformers 更适合:
text
本地实验
单请求验证
模型效果测试
vLLM 更适合:
text
线上推理
高并发服务
OpenAI兼容接口
多用户访问场景
如果你的服务已经出现:
text
QPS低
GPU利用率低
请求排队
P95延迟高
那不要只想着换更贵的 GPU。
很多时候真正要换的不是机器,而是推理架构。
十八、后续优化方向
后面还可以继续做:
text
1. Redis队列削峰
2. 多模型路由
3. 多GPU tensor parallel
4. Prometheus监控GPU与接口耗时
5. Kubernetes GPU自动扩缩容
6. vLLM多副本负载均衡
7. 请求优先级调度
这也是大模型系统从 Demo 到生产环境必须经历的一步。