强曰为道

与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

10 - 性能调优

10 - 性能调优

通过系统化的调优策略,最大化 vLLM 推理引擎的性能。


10.1 性能指标体系

10.1.1 核心指标

指标英文含义优化方向
首 Token 延迟TTFT (Time to First Token)从请求到收到第一个 token 的时间越低越好
每 Token 延迟TPOT (Time Per Output Token)生成每个 token 的平均时间越低越好
端到端延迟E2E Latency整个请求的完成时间越低越好
吞吐量Throughput每秒处理的 token 数越高越好
GPU 利用率GPU UtilizationGPU 计算单元的使用率越高越好
队列等待时间Queue Time请求在队列中等待的时间越低越好

10.1.2 指标关系

吞吐量 vs 延迟(典型关系):

延迟 ↑
  │  ╱
  │ ╱
  │╱    ← 延迟随负载增加
  ├──────────────── → 负载

吞吐量 ↑
  │        ┌─── 平台期
  │       ╱
  │      ╱
  │     ╱
  │    ╱
  └─────────────── → 负载

关键:找到吞吐量最高且延迟可接受的工作点

10.2 基准测试工具

10.2.1 使用内置 Benchmark

# vLLM 自带的基准测试工具
python -m vllm.entrypoints.openai.api_server_benchmark \
    --model Qwen/Qwen2.5-7B-Instruct \
    --dataset-path ShareGPT_V3_unfiltered_cleaned_split.json \
    --num-prompts 1000 \
    --request-rate 10 \
    --backend openai-chat

10.2.2 自定义基准测试

# benchmark.py
"""vLLM 性能基准测试"""

import time
import json
import statistics
import concurrent.futures
from openai import OpenAI

client = OpenAI(base_url="http://localhost:8000/v1", api_key="none")

def benchmark_throughput(prompts: list[str], model: str, max_tokens: int = 128):
    """测试吞吐量"""
    
    ttfts = []       # Time to First Token
    e2e_latencies = []  # End-to-end latency
    total_tokens = 0
    
    def send_request(prompt: str) -> dict:
        start = time.time()
        first_token_time = None
        
        stream = client.chat.completions.create(
            model=model,
            messages=[{"role": "user", "content": prompt}],
            max_tokens=max_tokens,
            stream=True,
        )
        
        tokens = []
        for chunk in stream:
            if first_token_time is None:
                first_token_time = time.time()
            if chunk.choices and chunk.choices[0].delta.content:
                tokens.append(chunk.choices[0].delta.content)
        
        end = time.time()
        
        return {
            "ttft": first_token_time - start if first_token_time else None,
            "e2e": end - start,
            "tokens": len(tokens),
        }
    
    # 并发发送所有请求
    start_time = time.time()
    with concurrent.futures.ThreadPoolExecutor(max_workers=20) as executor:
        futures = [executor.submit(send_request, p) for p in prompts]
        results = [f.result() for f in concurrent.futures.as_completed(futures)]
    total_time = time.time() - start_time
    
    # 统计
    for r in results:
        if r["ttft"]:
            ttfts.append(r["ttft"])
        e2e_latencies.append(r["e2e"])
        total_tokens += r["tokens"]
    
    print(f"=== 基准测试结果 ===")
    print(f"请求数: {len(prompts)}")
    print(f"总耗时: {total_time:.2f}s")
    print(f"总 tokens: {total_tokens}")
    print(f"吞吐量: {total_tokens / total_time:.1f} tokens/s")
    print(f"TTFT - avg: {statistics.mean(ttfts)*1000:.0f}ms, "
          f"p50: {statistics.median(ttfts)*1000:.0f}ms, "
          f"p99: {sorted(ttfts)[int(len(ttfts)*0.99)]*1000:.0f}ms")
    print(f"E2E  - avg: {statistics.mean(e2e_latencies)*1000:.0f}ms, "
          f"p50: {statistics.median(e2e_latencies)*1000:.0f}ms, "
          f"p99: {sorted(e2e_latencies)[int(len(e2e_latencies)*0.99)]*1000:.0f}ms")

if __name__ == "__main__":
    # 准备测试数据(模拟不同长度的 prompt)
    prompts = [
        f"用 {50 + i*10} 个字解释人工智能的未来发展趋势。"
        for i in range(100)
    ]
    
    benchmark_throughput(prompts, "qwen-7b")

10.2.3 ShareGPT 数据集测试

# benchmark_sharegpt.py
"""使用 ShareGPT 数据集进行基准测试"""

import json

def load_sharegpt(path: str, max_prompts: int = 1000):
    """加载 ShareGPT 数据集"""
    with open(path) as f:
        data = json.load(f)
    
    prompts = []
    for conv in data["conversations"][:max_prompts]:
        for turn in conv["conversations"]:
            if turn["from"] == "human":
                prompts.append(turn["value"])
                break
    
    return prompts

prompts = load_sharegpt("ShareGPT_V3_unfiltered_cleaned_split.json")
benchmark_throughput(prompts, "qwen-7b")

10.3 关键参数调优

10.3.1 GPU 显存利用率

# gpu_memory_utilization 调优

# 太低(0.7):浪费显存,KV Cache 空间少
llm = LLM(model="model", gpu_memory_utilization=0.7)
# KV Cache 块数: ~3000 → 并发数低

# 推荐(0.9):平衡安全和性能
llm = LLM(model="model", gpu_memory_utilization=0.9)
# KV Cache 块数: ~4500 → 并发数高

# 极限(0.95):最大 KV Cache,有 OOM 风险
llm = LLM(model="model", gpu_memory_utilization=0.95)
# KV Cache 块数: ~5000 → 最大并发,但有风险

10.3.2 最大序列长度

# max_model_len 调优

# 全长度(32K):每个请求分配更多 KV Cache 块
llm = LLM(model="model", max_model_len=32768)
# 并发数: ~30

# 中等长度(8K):平衡
llm = LLM(model="model", max_model_len=8192)
# 并发数: ~120

# 短长度(2K):最大化并发
llm = LLM(model="model", max_model_len=2048)
# 并发数: ~500

10.3.3 批处理参数

llm = LLM(
    model="model",
    
    # 最大并发序列数
    # 增大 → 更多并发,但每步计算更慢
    max_num_seqs=256,
    
    # 最大批处理 token 数
    # 增大 → 更高吞吐,但每步更慢
    max_num_batched_tokens=8192,
    
    # Swap 空间(GB)
    # 增大 → 更多可 swap 的序列,但 CPU 内存占用增加
    swap_space=4,
)

10.3.4 参数调优矩阵

场景gpu_memory_utilizationmax_model_lenmax_num_seqsmax_num_batched_tokens
高吞吐离线0.95204851216384
平衡在线0.9040961288192
低延迟实时0.852048324096
长上下文0.9232768168192

10.4 缓存策略

10.4.1 前缀缓存(Prefix Caching)

# 启用前缀缓存(对共享 system prompt 场景效果显著)
vllm serve model --enable-prefix-caching

效果示例

场景:100 个请求共享相同的系统提示词(500 tokens)

无前缀缓存:
  Prefill 总 tokens = 500 × 100 = 50,000

有前缀缓存:
  首次 Prefill = 500 + 用户问题
  后续 Prefill = 0(缓存命中)+ 用户问题
  Prefill 总 tokens ≈ 500 + 用户问题 × 100 ≈ 5,000
  
  TTFT 改善: 90%↓

10.4.2 CUDA Graph 优化

# CUDA Graph 默认启用,可显著减少 kernel launch 开销
# 如遇兼容性问题,可禁用
vllm serve model --enforce-eager  # 禁用 CUDA Graph(性能下降 10-30%)

10.4.3 KV Cache 量化

# FP8 KV Cache(需要 H100/H200)
vllm serve model --kv-cache-dtype fp8

# 效果:KV Cache 内存减半,可支持约 2x 的并发数
# 代价:极小的精度损失

10.5 模型优化

10.5.1 数据类型选择

# dtype 对性能的影响

llm = LLM(model="model", dtype="auto")      # 自动选择
llm = LLM(model="model", dtype="float16")   # FP16
llm = LLM(model="model", dtype="bfloat16")  # BF16(A100/H100 推荐)
dtypeA100H100RTX 4090显存
float16✅ 快✅ 快✅ 快2x
bfloat16最快最快2x
float32✅ 慢✅ 慢✅ 慢4x

10.5.2 量化权衡

# 量化方案性能对比

# FP16(基线)
llm = LLM(model="Qwen2.5-7B", dtype="float16")
# 吞吐量: 100%, 精度: 最高

# AWQ 4-bit
llm = LLM(model="Qwen2.5-7B-AWQ", quantization="awq")
# 吞吐量: 90-110%, 精度: 较高, 显存: -75%

# FP8
llm = LLM(model="Qwen2.5-7B", quantization="fp8")
# 吞吐量: 95-100%, 精度: 极高, 显存: -50%

10.6 系统级优化

10.6.1 操作系统优化

# 1. 关闭透明大页(THP)
echo never > /sys/kernel/mm/transparent_hugepage/enabled

# 2. 调整文件描述符限制
ulimit -n 65535

# 3. 调整共享内存
sudo mount -o remount,size=32G /dev/shm

# 4. GPU 持久化模式
sudo nvidia-smi -pm 1

# 5. 设置 CPU 性能模式
sudo cpupower frequency-set -g performance

# 6. 绑定 NUMA 节点
numactl --cpunodebind=0 --membind=0 vllm serve model

10.6.2 NVIDIA 驱动优化

# 设置计算模式
sudo nvidia-smi -c EXCLUSIVE_PROCESS

# 设置功率限制(平衡性能和功耗)
sudo nvidia-smi -pl 350  # 350W for A100

# 启用 MIG(Multi-Instance GPU,仅 A100/H100)
# 将 1 个 A100 切分为多个独立实例
sudo nvidia-smi mig -cgi 9,9,9,9 -C

10.6.3 Python 环境优化

# 设置线程数
export OMP_NUM_THREADS=8
export MKL_NUM_THREADS=8

# 禁用 Python GC(减少 GC 暂停)
export PYTHONDONTWRITEBYTECODE=1

# 使用 UVLoop(更快的事件循环)
pip install uvloop

10.7 吞吐量优化清单

10.7.1 快速优化

□ 使用 BF16(A100/H100)
□ 启用前缀缓存(如果有共享 prompt)
□ 使用量化模型(AWQ/FP8)
□ 减小 max_model_len 到实际需要的长度
□ 增大 gpu_memory_utilization 到 0.9-0.95
□ 使用 NVLink 连接的 GPU 进行张量并行
□ 启用 Chunked Prefill(长 prompt 场景)

10.7.2 深度优化

□ 调整 max_num_seqs 和 max_num_batched_tokens
□ 使用 FP8 KV Cache
□ 调整 swap_space
□ 系统级 NUMA 绑定
□ 使用 CUDA Graph(默认启用)
□ 调整 NCCL 通信参数(分布式场景)
□ 使用 Ray 进行多节点调度

10.8 延迟优化

10.8.1 TTFT 优化

# 减少首 token 延迟

# 1. 启用 Chunked Prefill
llm = LLM(
    model="model",
    enable_chunked_prefill=True,
    max_num_batched_tokens=512,  # 较小的 chunk
)

# 2. 减小 prompt 长度(使用更简洁的系统提示)

# 3. 使用前缀缓存(避免重复 prefill)

# 4. 限制 max_num_seqs(减少批大小对延迟的影响)

10.8.2 TPOT 优化

# 减少每 token 延迟

# 1. 减少批大小(降低每步计算量)

# 2. 使用量化(减少计算量)

# 3. 使用张量并行(分摊计算)

10.9 性能对比参考

10.9.1 典型吞吐量数据

模型GPUTP吞吐量 (tok/s)并发数
Qwen2.5-7B1x A10011,500-2,00050
Qwen2.5-14B1x A1001800-1,20030
Qwen2.5-72B4x A1004500-80020
LLaMA-3.1-70B4x A1004400-70020
LLaMA-3.1-70B-AWQ2x A1002300-50040
LLaMA-3.1-405B8x H1008200-40010

:以上数据为参考值,实际性能取决于 prompt 长度、生成长度、并发数等因素。


10.10 注意事项

过犹不及gpu_memory_utilization 设得过高(如 0.98)可能导致 OOM。建议留 5-10% 的余量。

基准测试:不要凭经验猜测,用实际工作负载进行基准测试。不同 prompt 分布的性能差异很大。

监控先行:在调优之前先建立监控(参见第 11 章),否则无法量化改善效果。

逐步调优:一次只改变一个参数,观察效果后再继续。


10.11 扩展阅读


上一章09 - 分布式推理 | 下一章11 - 监控与可观测性