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数
}

下一步

目前单用户延迟还可以,但高并发下还是有抖动。准备尝试:

  1. 量化推理:4bit量化进一步降低显存占用
  2. 多模型并行:不同模型处理不同类型请求
  3. 智能路由:根据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/) 转载或引用必须申明原指尖魔法屋来源及源地址!