AI模型服务性能踩坑记录
线上 Llama2-7B 对话服务跑在 AWS g4dn.xlarge(单卡 T4)上,用户抱怨回答慢:首 token 延迟 2–3 秒,完整回复 5–10 秒才出完。
我第一反应是模型太大、卡不够。翻监控才发现 GPU 利用率长期徘徊在 40% 左右——显然不是算力顶满的问题,得从别处找原因。
初识性能问题
场景背景
线上跑着一个基于Llama2-7B的对话服务,部署在AWS g4dn.xlarge实例上(1张T4 GPU),用户反馈回答太慢,首token延迟在2-3秒,完整响应要5-10秒。
第一反应是:模型太大,硬件不够。但翻了监控数据发现,GPU利用率才40%,显然不是算力瓶颈。
# 监控GPU状态
nvidia-smi -l 1
# 查看模型加载情况
ls -lh /data/models/llama2-7b
# 总大小13.5GB,占用显存11.2GB
问题定位
先做了简单的压力测试:
# 使用locust压测
cat locustfile.py
from locust import HttpUser, task, between
class AIModelUser(HttpUser):
wait_time = between(1, 3)
@task
def chat_completion(self):
self.client.post("/v1/chat/completions", json={
"model": "llama2-7b",
"messages": [
{"role": "user", "content": "写一个Python函数计算斐波那契数列"}
],
"max_tokens": 512
})
# 启动压测
locust -f locustfile.py --users 10 --host http://localhost:8000
结果:
- 单用户时首token延迟800ms,总延迟1.2s
- 10用户并发时首token延迟跳到2.5s,总延迟8s+
- GPU利用率从40%跳到85%
说明问题不是单次推理慢,而是并发处理能力不足。
优化之路
1. 请求批处理
第一个尝试是启用请求批处理(batching),将多个请求合并处理:
# 原始推理服务
from vllm import LLM, SamplingParams
llm = LLM(model="meta-llama/Llama-2-7b-hf", tensor_parallel_size=1)
def generate(prompt: str):
sampling_params = SamplingParams(temperature=0.7, max_tokens=512)
outputs = llm.generate([prompt], sampling_params)
return outputs[0].outputs[0].text
# 优化后:启用批处理
from vllm import LLM, SamplingParams
llm = LLM(
model="meta-llama/Llama-2-7b-hf",
tensor_parallel_size=1,
max_num_batched_tokens=4096, # 每批最大token数
max_num_seqs=32, # 每批最大请求数
)
async def generate(prompts: list[str]):
sampling_params = SamplingParams(temperature=0.7, max_tokens=512)
outputs = llm.generate(prompts, sampling_params)
return [output.outputs[0].text for output in outputs]
效果:
- 并发10用户时首token延迟降到1.2s
- 总延迟降到3-4s
- GPU利用率稳定在90%+
2. KV Cache优化
vLLM默认开启了KV Cache,但可以进一步优化:
llm = LLM(
model="meta-llama/Llama-2-7b-hf",
tensor_parallel_size=1,
max_num_batched_tokens=4096,
max_num_seqs=32,
block_size=16, # KV cache块大小,默认16
gpu_memory_utilization=0.9, # 显存利用率
swap_space=4, # swap到磁盘的GB数
enable_prefix_caching=True, # 启用前缀缓存
)
实测:
- 显存占用从11.2GB降到10.5GB
- 相同请求重复时首token延迟再降30%
- 但需要监控swap带来的延迟抖动
3. 流式输出
改成流式输出,提升用户体验:
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import asyncio
app = FastAPI()
@app.post("/v1/chat/completions")
async def chat_completions(request: ChatRequest):
async def stream_generator():
sampling_params = SamplingParams(
temperature=0.7,
max_tokens=512,
stream=True,
)
for output in llm.generate_stream([request.messages[-1].content], sampling_params):
if output.outputs:
token = output.outputs[0].text
yield f"data: {json.dumps({'choices': [{'delta': {'content': token}}]})}\n\n"
return StreamingResponse(stream_generator(), media_type="text/event-stream")
用户体验明显改善,虽然总延迟没变,但首token就能看到输出。
4. 负载均衡
单机毕竟有极限,上了负载均衡:
# docker-compose.yml
version: '3.8'
services:
model-server-1:
image: vllm/vllm-openai:latest
ports:
- "8001:8000"
environment:
- MODEL=meta-llama/Llama-2-7b-hf
- GPU_MEMORY_UTILIZATION=0.8
model-server-2:
image: vllm/vllm-openai:latest
ports:
- "8002:8000"
environment:
- MODEL=meta-llama/Llama-2-7b-hf
- GPU_MEMORY_UTILIZATION=0.8
nginx:
image: nginx:latest
ports:
- "8000:80"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
# nginx.conf
upstream model_servers {
least_conn; # 最少连接数分配
server model-server-1:8000;
server model-server-2:8000;
}
server {
listen 80;
location / {
proxy_pass http://model_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
吞吐量翻倍,但单用户延迟反而增加了(负载均衡开销)。
踩过的坑
坑1:批处理size设置不当
一开始把max_num_seqs设成128,想一次处理更多请求,结果:
# 监控显示
nvidia-smi
# 显存爆满:OOM
原因:显存不够支撑那么大batch。调小到32就稳定了。
坑2:流式输出超时
# 错误代码
timeout = 30 # 固定30秒超时
async with aiohttp.ClientSession(timeout=aiohttp.ClientTimeout(total=timeout)) as session:
async for chunk in session.post(url, json=data):
...
# 问题:长推理场景30秒不够
# 修复:动态计算超时
timeout = 30 + len(prompt) * 0.01 + max_tokens * 0.05
坑3:负载均衡不均衡
用轮询(round_robin)时发现有些实例很忙有些很闲:
# 错误配置
upstream model_servers {
server model-server-1:8000;
server model-server-2:8000;
}
# 修复:用least_conn
upstream model_servers {
least_conn;
server model-server-1:8000;
server model-server-2:8000;
}
坑4:GPU利用率虚高
监控显示GPU利用率95%,以为接近极限,但实际吞吐量很低。
# 细看发现
nvidia-smi -q -d UTILIZATION
# SM利用率:95%
# Memory Bandwidth利用率:20%
# 说明计算够,但内存带宽是瓶颈
改用半精度(fp16)减少内存带宽压力:
llm = LLM(
model="meta-llama/Llama-2-7b-hf",
dtype="half", # 改用fp16
...
)
当前架构
优化后的整体架构:
Client
↓
Nginx (负载均衡)
↓
vLLM Server 1 (g4dn.xlarge)
vLLM Server 2 (g4dn.xlarge)
↓
GPU (T4 x 2)
关键配置:
# vLLM配置
CONFIG = {
"model": "meta-llama/Llama-2-7b-hf",
"tensor_parallel_size": 1,
"max_num_batched_tokens": 4096,
"max_num_seqs": 32,
"block_size": 16,
"gpu_memory_utilization": 0.85,
"swap_space": 2,
"enable_prefix_caching": True,
"dtype": "half",
"max_model_len": 2048,
}
# 性能指标
METRICS = {
"p95_latency_ms": 3500, # 95分位延迟
"throughput_req/s": 25, # 每秒请求数
"gpu_utilization": 0.82, # GPU利用率
"token_throughput": 1200, # 每秒生成token数
}
下一步
目前单用户延迟还可以,但高并发下还是有抖动。准备尝试:
- 量化推理:4bit量化进一步降低显存占用
- 多模型并行:不同模型处理不同类型请求
- 智能路由:根据prompt长度和复杂度路由到不同实例
性能优化没有终点,只有不断逼近的极限。
脚本收藏
几个常用的性能测试脚本:
# 延迟测试脚本
#!/bin/bash
echo "Testing latency..."
for i in {1..100}; do
start=$(date +%s%N)
curl -s -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"llama2-7b","messages":[{"role":"user","content":"Hello"}],"max_tokens":10}' > /dev/null
end=$(date +%s%N)
echo "Request $i: $((($end - $start) / 1000000))ms"
done
# 吞吐量测试脚本
#!/bin/bash
echo "Testing throughput..."
for i in {1..50}; do
curl -s -X POST http://localhost:8000/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{"model":"llama2-7b","messages":[{"role":"user","content":"Hello"}],"max_tokens":10}' > /dev/null &
done
wait
echo "Completed 50 requests"
# GPU监控脚本
#!/bin/bash
watch -n 1 "nvidia-smi --query-gpu=timestamp,utilization.gpu,utilization.memory,memory.used,memory.total --format=csv,noheader,nounits"
优化是个过程,不是目的地。
版权声明: 本文首发于 指尖魔法屋-AI模型服务性能踩坑记录(https://blog.thinkmoon.cn/post/256-ai-model-service-performance-latency-throughput-optimization/) 转载或引用必须申明原指尖魔法屋来源及源地址!
评论
使用 GitHub 账号登录后即可留言,支持 Markdown。