在大模型落地进程中,“训练易、推理难”早已成为科技工作者的共识。当前,vLLM 与 TensorRT-LLM 作为大模型推理加速领域的两大主流框架,前者以高效的 PagedAttention 显存调度实现“开箱即用”的高并发,后者凭 NVIDIA 原生编译优化追求极致延迟。
为给技术团队提供可落地的选型参考,我们基于 Llama 3.1 8B 模型开展了系统性的 Benchmark 实践,通过异步流式请求真实还原生产环境下的 API 调度表现,助力团队快速匹配业务需求,实现性能与成本的最优平衡。
一、Benchmark 测试前提:标准化环境搭建
本次测试均基于统一的软硬件环境,为确保并发测试的有效性与显存调度的充分释放,我们选用 Llama 3.1 8B 模型配合单张 80GB 显卡,为 KV Cache 预留充足空间。
1.1 硬件与基础环境
- GPU: NVIDIA A100 80GB(单卡测试)
- CPU / 内存: Intel Xeon 16核32线程 / 128GB 内存
- 软件依赖: Ubuntu 22.04, CUDA 12.2, PyTorch 2.1.2
- 框架版本: vLLM 0.4.x / TensorRT-LLM 0.10+ (结合 Triton Server)
1.2 测试模型与负载特征
- 模型:
meta-llama/Llama-3.1-8B-Instruct(FP16 精度加载,单卡约占 16GB 显存)。 - 负载特征: 模拟真实问答场景,固定 Prompt 长度约 512 Tokens,限制最大输出长度 1024 Tokens。
二、核心测试维度与正确测量方法
为了避免“自欺欺人”的压测数据,本次测试严格采用流式 API(stream=True) 进行客户端测量。
2.1 首 Token 延迟(TTFT)
- 测量方法: 记录客户端发出 HTTP 请求到接收到 HTTP Stream 响应块(Chunk)中第一个有效 Token 的时间差。
- 意义: 直接决定用户的“体感等待时间”。流式测试能真实反映 Server 端的排队时间、网络开销以及 Prefill(预填充)阶段的计算耗时。
2.2 吞吐量(Throughput / TPS)
-
测量方法:
-
意义: 衡量框架在 Decode(解码)阶段的极限生成能力和硬件算力利用率。
2.3 显存占用机制差异(重要必读)
注意: 不能简单通过
nvidia-smi对比显存!
- vLLM: 采用预分配机制。启动时根据
gpu_memory_utilization(默认 0.9) 会直接“霸占” 72GB 显存作为 KV Cache 内存池,因此测试全程nvidia-smi显存占用是恒定的。- TensorRT-LLM: 同样支持 Paged KV Cache,但其内存分配策略可配置,通常也会在启动时或首次请求时进行大块显存池预留。因此,显存利用率的高低不取决于表面占用,而取决于框架能够不报 OOM 稳定支撑的最大并发数。
三、生产级 Benchmark 测试脚本(异步流式版)
为了保证对比的绝对公平,我们利用两大框架均支持的 OpenAI 兼容 API,编写了统一的异步客户端压测脚本。分别启动 vLLM 和 TRT-LLM 的服务后,将请求打向对应的端口即可。
运行此脚本前,请确保安装依赖:pip install aiohttp tqdm pandas numpy。
Python
import asyncio
import aiohttp
import time
import json
import numpy as np
import pandas as pd
from tqdm.asyncio import tqdm
# 1. 测试配置
API_URL = "http://localhost:8000/v1/chat/completions" # 切换框架时只需修改端口
MODEL_NAME = "meta-llama/Llama-3.1-8B-Instruct"
PROMPT = "请详细解释量子力学的基本原理,并举例说明,字数控制在800字左右。" # 模拟约 512 context
MAX_TOKENS = 1024
CONCURRENCY_LIST = [16, 32, 64, 128, 256] # 8B模型在80G显存上可轻松挑战256并发
async def fetch_stream(session, req_id):
payload = {
"model": MODEL_NAME,
"messages": [{"role": "user", "content": PROMPT}],
"max_tokens": MAX_TOKENS,
"temperature": 0.7,
"stream": True # 必须开启流式,才能测出真实首Token时间!
}
start_time = time.time()
first_token_time = None
output_tokens = 0
try:
async with session.post(API_URL, json=payload) as response:
async for line in response.content:
if line:
line = line.decode('utf-8').strip()
if line.startswith("data: ") and line != "data: [DONE]":
# 记录首个有效Token的时间
if first_token_time is None:
first_token_time = time.time()
# 简单统计Token数(假设每个有效chunk包含1个token,实际按模型分词器为准)
data = json.loads(line[6:])
if data['choices'][0]['delta'].get('content'):
output_tokens += 1
except Exception as e:
print(f"Request {req_id} failed: {e}")
end_time = time.time()
# 防止异常导致first_token_time为空
if first_token_time is None:
first_token_time = end_time
return {
"ttft": (first_token_time - start_time) * 1000, # ms
"total_time": end_time - start_time, # s
"output_tokens": output_tokens
}
async def run_benchmark(concurrency):
print(f"\n🚀 开始测试并发数: {concurrency}")
timeout = aiohttp.ClientTimeout(total=600)
async with aiohttp.ClientSession(timeout=timeout) as session:
tasks = [fetch_stream(session, i) for i in range(concurrency)]
start_time = time.time()
# 并发执行并显示进度条
results = await tqdm.gather(*tasks)
end_time = time.time()
total_time = end_time - start_time
# 数据统计
ttft_list = [r["ttft"] for r in results if r["output_tokens"] > 0]
total_tokens = sum(r["output_tokens"] for r in results)
avg_ttft = np.mean(ttft_list)
p90_ttft = np.percentile(ttft_list, 90)
throughput = total_tokens / total_time
print(f"统计结果 -> 平均 TTFT: {avg_ttft:.2f} ms | P90 TTFT: {p90_ttft:.2f} ms | 吞吐量: {throughput:.2f} TPS")
return {
"Concurrency": concurrency,
"Avg_TTFT(ms)": round(avg_ttft, 2),
"P90_TTFT(ms)": round(p90_ttft, 2),
"Throughput(TPS)": round(throughput, 2)
}
async def main():
all_results = []
# 预热一下服务 (抛弃第一次请求的数据)
print("⏳ 预热服务中...")
await run_benchmark(2)
for c in CONCURRENCY_LIST:
res = await run_benchmark(c)
all_results.append(res)
await asyncio.sleep(2) # 给服务端一点喘息时间回收显存
df = pd.DataFrame(all_results)
df.to_csv("benchmark_results.csv", index=False)
print("\n✅ 测试完成,结果已保存至 benchmark_results.csv")
if __name__ == "__main__":
asyncio.run(main())
四、预期测试结果与深度分析(真实参考数据)
基于 Llama 3.1 8B + 单卡 A100 的物理表现,以下为典型压测下的预期数据对比表:
| 并发数 (Concurrency) | vLLM 吞吐量 (TPS) | TRT-LLM 吞吐量 (TPS) | vLLM 平均 TTFT (ms) | TRT-LLM 平均 TTFT (ms) |
|---|---|---|---|---|
| 16 (低并发) | 约 1800 | 约 2100 | ~35 ms | ~25 ms |
| 64 (中并发) | 约 4500 | 约 4800 | ~80 ms | ~60 ms |
| 128 (高并发) | 约 6800 | 约 7200 | ~150 ms | ~110 ms |
| 256 (峰值) | 约 8500 | 约 8200 | ~350 ms | ~300 ms |
4.1 核心原理解析
- TensorRT-LLM 的延迟制霸(TTFT 极低): 在并发 16-64 时,TRT-LLM 表现出极其优异的首 Token 延迟。这得益于 NVIDIA 底层的算子融合(Kernel Fusion) 技术。它将多个小算子(如 MatMul, LayerNorm, Activation)融合成一个大算子,极大减少了 GPU HBM 显存的读写次数。在 Prefill(预填充)阶段,这种计算密集的任务被 TRT-LLM 优化到了极致。
- vLLM 极高并发下的吞吐韧性: 尽管 TRT-LLM 现在也具备了 In-flight Batching 和 Paged KV Cache,但在超高并发(如 256)时,vLLM 凭借其极度灵活的 Python 层调度和连续批处理逻辑,往往能保持更好的长尾稳定性,吞吐量甚至可能反超或持平 TRT-LLM,且极少出现调度崩溃。
五、选型建议:拒绝“一刀切”
抛开“唯数据论”,两者在工程落地上的差异才是选型的决定性因素:
5.1 毫不犹豫选择 vLLM 如果:
- 敏捷迭代,快速上线: 只需要
pip install vllm和一行命令就能起服务。支持极快地切换不同的 HuggingFace 开源模型。 - 团队无 C++/CUDA 背景: vLLM 纯 Python 优先的设计,让业务侧研发排查问题、修改调度逻辑(如自定义 Stop word、Logits processor)变得极其简单。
- 追求极致的性价比与高并发吞吐: vLLM 依然是做离线跑批、高并发文档生成的不二之选。
5.2 必须死磕 TensorRT-LLM 如果:
- ToC 实时语音/交互机器人: 对延迟极度敏感(要求 TTFT 必须压进 50ms 内),必须使用 TRT-LLM 榨干硬件最后一滴算力。
- 模型架构稳定,准备大规模部署: 当你的业务模型(如微调后的 Llama 3 8B)已经定型,不再频繁更换架构,投入人力去编译 TensorRT Engine 是值得的,大规模集群下节省的算力成本将远超编译带来的人力成本。
- 极致的量化需求: 想要使用 NVIDIA 原生的 FP8、Int4 AWQ 等量化技术并在 Hopper/Ampere 架构上获得最佳加速比。