LLM推理引擎实战横评:vLLM、SGLang、TensorRT-LLM 在 H100 上的真实表现
上个月接了个活,把公司内部的 Llama 3.3 70B 从 Triton 迁移到专业推理引擎。三个候选方案:vLLM、SGLang、TensorRT-LLM。测完之后发现,网上大部分对比文章要么只贴官方数据,要么测试条件含糊。这篇把我实际跑的数据和踩的坑都记下来,给同样在选型的人省点时间。
先说结论
| 指标 | vLLM v0.18.0 | SGLang v0.5.9 | TensorRT-LLM v1.2.0 |
|---|---|---|---|
| 吞吐量(50并发) | 1,850 tok/s | 1,920 tok/s | 2,100 tok/s |
| TTFT p50(10并发) | 120 ms | 112 ms | 105 ms |
| TTFT p95(100并发) | 1,450 ms | 1,380 ms | 1,280 ms |
| 冷启动 | ~62s | ~58s | ~28min(首次编译) |
| 闲置显存 | 71 GB | 72 GB | 74 GB |
| 峰值显存(100并发) | 78 GB | 78 GB | 79 GB |
TensorRT-LLM 吞吐量最高,但首次编译要 28 分钟。SGLang 在共享前缀场景下优势明显。vLLM 最省心,模型支持最广。
选哪个取决于你的场景,不存在"最好"的引擎。下面展开说。
测试环境
硬件是单卡 H100 SXM5 80GB,驱动 590.48.01。vLLM 和 SGLang 用 CUDA 13.0 容器,TensorRT-LLM 用 CUDA 13.1.0(pytorch:25.12-py3 镜像)。
模型统一用 meta-llama/Llama-3.3-70B-Instruct,FP8 精度。FP8 下权重约 70GB,80GB 显存刚好能塞下,但 KV cache 空间比较紧张。
测试方法:异步 Python 客户端,200 条 prompt,平均输入 512 token、输出 256 token,固定随机种子。每个并发级别跑 3 分钟,前 60 秒预热不计入统计。
三个引擎的核心差异
vLLM:PagedAttention 的发明者
vLLM 的核心卖点是 PagedAttention------把 KV cache 拆成固定大小的页(page),按需分配,用完回收。这个思路借鉴了操作系统的虚拟内存管理,解决了一个很实际的问题:静态预分配 KV cache 会浪费 60-90% 的显存。
启动只要几行:
bash
# 安装
pip install vllm
# 启动 FP8 推理服务
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3.3-70B-Instruct \
--quantization fp8 \
--tensor-parallel-size 1 \
--max-model-len 4096 \
--gpu-memory-utilization 0.92
vLLM 的 FP8 量化是在线动态量化(on-load),不需要提前跑量化脚本。--quantization fp8 一个参数搞定。模型支持最广,截至 v0.18.0 覆盖了几百种架构,包括多模态模型(Qwen3-VL、InternVL3、Pixtral 等)。
踩坑记录 1: --gpu-memory-utilization 默认 0.9,在 70B FP8 + H100 80GB 的组合下,--max-model-len 超过 8192 就会 OOM。改成 0.92 可以勉强跑到 8192,但建议 4096 更稳定。如果你需要更长的上下文,要么上多卡 tensor parallel,要么用 INT4 量化。
SGLang:RadixAttention 的前缀缓存
SGLang 和 vLLM 最大的区别在 KV cache 管理策略。vLLM 按请求独立管理 KV cache,SGLang 用 RadixAttention 在不同请求之间共享前缀的 KV cache。
什么意思?假设你有个 RAG 应用,系统 prompt 是 2000 token,每次请求都带着这 2000 token 的前缀。vLLM 每次请求都要重新计算这 2000 token 的 KV cache,SGLang 只算一次,后续请求直接复用。
bash
# 安装
pip install sglang[all]
# 启动
python -m sglang.launch_server \
--model meta-llama/Llama-3.3-70B-Instruct \
--quantization fp8 \
--tp 1 \
--context-length 4096
python
# Python SDK 用法,支持结构化输出
import sglang as sgl
@sgl.function
def multi_turn_chat(s, question1, question2):
s += sgl.system("你是一个技术专家。")
s += sgl.user(question1)
s += sgl.assistant(sgl.gen("answer1", max_tokens=256))
s += sgl.user(question2)
s += sgl.assistant(sgl.gen("answer2", max_tokens=256))
state = multi_turn_chat.run(
question1="什么是 KV cache?",
question2="它和注意力机制的关系?"
)
print(state["answer1"])
print(state["answer2"])
在我的测试中,如果所有请求共享 2000 token 的系统 prompt,SGLang 的吞吐量比 vLLM 高 35-40%。但如果每个请求的 prompt 完全不同(我上面的基准测试就是这个场景),SGLang 的 RadixAttention 没法发挥,吞吐量只比 vLLM 高 3-4%。
踩坑记录 2: SGLang 的 --context-length 参数不能省。省了之后它会用模型默认的最大长度(70B Instruct 是 131072),然后 KV cache 预分配直接把显存吃光。第一次部署的时候在这个问题上卡了半小时。
TensorRT-LLM:编译换速度
TensorRT-LLM 的思路完全不同:先把模型编译成优化过的 CUDA 引擎(TRT engine),运行时直接跑编译后的计算图。好处是所有优化(算子融合、内存布局、量化)在编译期就做完了,运行时开销最小。
bash
# 步骤1:量化
python quantize.py \
--model_dir meta-llama/Llama-3.3-70B-Instruct \
--output_dir ./llama70b-fp8 \
--qformat fp8
# 步骤2:编译引擎(这一步要28分钟)
trtllm-build \
--checkpoint_dir ./llama70b-fp8 \
--output_dir ./llama70b-engine \
--max_batch_size 128 \
--max_input_len 2048 \
--max_seq_len 4096
# 步骤3:启动服务
trtllm-serve ./llama70b-engine
编译一次,引擎文件保存到磁盘。下次启动加载编译好的引擎大概 90 秒,跳过 28 分钟的编译。但如果换模型版本、改最大序列长度、改批处理大小,都要重新编译。
v1.2.0 新增了 PyTorch 后端,可以跳过编译直接加载 HuggingFace 权重,冷启动降到 60-90 秒。代价是吞吐量比编译后的引擎低 15-20%。
踩坑记录 3: --max_batch_size 和 --max_input_len 在编译时就固定了。如果线上流量偶尔有超长请求(比如 8192 token),但编译时只设了 2048,这些请求会直接报错。建议编译时 --max_input_len 设成你实际最大输入长度的 1.5 倍。
踩坑记录 4: TensorRT-LLM 的 FP8 量化需要先跑 quantize.py,不像 vLLM 那样一个参数搞定。量化脚本依赖 modelopt 包,版本兼容性经常出问题。目前稳定的组合是 TensorRT-LLM v1.2.0 + modelopt 0.27.x。
吞吐量深度对比
贴一下不同并发级别的完整数据:
bash
并发数 vLLM SGLang TRT-LLM
1 120 tok/s 125 tok/s 130 tok/s
10 650 tok/s 680 tok/s 710 tok/s
50 1,850 1,920 2,100
100 2,400 2,460 2,780
几个观察:
低并发差距小。 1 并发时三者差距只有 8%。这时候瓶颈在模型本身的自回归解码,引擎优化空间有限。
高并发 TRT-LLM 拉开差距。 100 并发时 TRT-LLM 比 vLLM 快 16%。编译后的 CUDA 引擎在高负载下调度效率更高,算子融合减少了 kernel launch 的开销。
SGLang 的真正优势需要前缀命中。 上面的测试用的是完全不同的 prompt,SGLang 的 RadixAttention 基本没起作用。我额外跑了一组前缀共享测试:200 条请求共享 2048 token 的系统 prompt,SGLang 在 50 并发下跑到了 2,680 tok/s,比 vLLM 高 45%,甚至超过了 TRT-LLM。
TTFT(首 token 延迟)才是用户体验的关键
吞吐量决定成本,但 TTFT 决定用户体验。用户打开对话后等 700ms 和等 1500ms,感受完全不同。
bash
并发数 vLLM p50/p95 SGLang p50/p95 TRT-LLM p50/p95
1 45ms/68ms 42ms/61ms 38ms/55ms
10 120ms/195ms 112ms/178ms 105ms/170ms
50 380ms/720ms 360ms/680ms 340ms/620ms
100 740ms/1450ms 710ms/1380ms 680ms/1280ms
100 并发时 TRT-LLM 的 p95 TTFT 是 1280ms,vLLM 是 1450ms,差 170ms。对交互式应用来说,这个差距用户能感知到。
一段完整的部署脚本
贴一段我在生产环境用的 vLLM 部署脚本(Docker Compose),带健康检查和自动重启:
yaml
# docker-compose.yml
version: "3.8"
services:
vllm:
image: vllm/vllm-openai:v0.18.0
deploy:
resources:
reservations:
devices:
- driver: nvidia
count: 1
capabilities: [gpu]
ports:
- "8000:8000"
volumes:
- ./models:/models
command: >
--model /models/Llama-3.3-70B-Instruct
--quantization fp8
--max-model-len 4096
--gpu-memory-utilization 0.92
--served-model-name llama-70b
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:8000/health"]
interval: 30s
timeout: 10s
retries: 3
restart: unless-stopped
environment:
- VLLM_WORKER_MULTIPROC_METHOD=spawn
python
# benchmark.py - 简单的吞吐量测试脚本
import asyncio
import aiohttp
import time
API_URL = "http://localhost:8000/v1/completions"
MODEL = "llama-70b"
async def send_request(session, prompt):
payload = {
"model": MODEL,
"prompt": prompt,
"max_tokens": 256,
"temperature": 0.7
}
start = time.monotonic()
async with session.post(API_URL, json=payload) as resp:
result = await resp.json()
elapsed = time.monotonic() - start
tokens = result["usage"]["completion_tokens"]
return tokens, elapsed
async def benchmark(concurrency=50, total_requests=200):
prompts = [f"Explain concept #{i} in distributed systems" for i in range(total_requests)]
semaphore = asyncio.Semaphore(concurrency)
total_tokens = 0
total_time = 0
async def bounded_request(session, prompt):
nonlocal total_tokens, total_time
async with semaphore:
tokens, elapsed = await send_request(session, prompt)
total_tokens += tokens
total_time = max(total_time, elapsed)
async with aiohttp.ClientSession() as session:
wall_start = time.monotonic()
tasks = [bounded_request(session, p) for p in prompts]
await asyncio.gather(*tasks)
wall_time = time.monotonic() - wall_start
print(f"并发: {concurrency}")
print(f"总请求: {total_requests}")
print(f"总 token: {total_tokens}")
print(f"墙钟时间: {wall_time:.1f}s")
print(f"吞吐量: {total_tokens/wall_time:.0f} tok/s")
asyncio.run(benchmark())
选型决策树
经过这轮测试,我总结了一个选型逻辑:
选 vLLM 的情况:
- 模型换得勤(每周或每月换新模型)
- 需要支持多种模型架构(文本 + 多模态)
- 团队对推理引擎不熟,希望上手门槛低
- 有 auto-scaling 需求,冷启动要快
选 SGLang 的情况:
- 业务场景有大量共享前缀(RAG、多轮对话、batch 推理用相同 system prompt)
- 需要结构化输出(JSON mode)
- 对 TTFT 敏感,用户直接面对推理服务
选 TensorRT-LLM 的情况:
- 模型长期固定不换
- 追求极致吞吐量,硬件利用率优先
- 有专人维护推理基础设施
- 不需要频繁 auto-scaling
一个常见的误区:觉得 TRT-LLM 一定最快。在前缀共享场景下,SGLang 的 RadixAttention 可以反超 TRT-LLM。引擎的"快"取决于你的 workload 特征。
优化建议
不管选哪个引擎,有几个通用的优化手段:
1. Prompt Caching 如果用 API 服务(OpenAI、Anthropic、Google),它们都有 prompt caching 功能。缓存命中后,前缀部分的延迟降低 80-90%。自建推理的话,SGLang 内置了 RadixAttention,vLLM 可以用 --enable-prefix-caching。
2. FP8 优先于 INT4 H100 原生支持 FP8,吞吐量几乎不受影响,精度损失小于 1%。INT4(GPTQ/AWQ)虽然显存省更多,但需要额外的反量化计算,实际吞吐量可能比 FP8 还低。
3. 调整 max_model_len 别用模型默认的最大长度。70B Instruct 默认 131072 token,KV cache 预分配会直接吃光显存。根据实际业务场景设成够用的长度(大部分场景 4096-8192 足够),省出来的显存可以跑更多并发。
4. Continuous Batching 参数调优 vLLM 的 --max-num-seqs(最大并发序列数)和 --max-num-batched-tokens(单次 batch 最大 token 数)需要根据显存和延迟要求调整。默认值偏保守,适当调大可以提升 20-30% 的吞吐量。
后续关注
几个值得关注的趋势:
Modular MAX 用 Mojo 编写了自定义 GPU kernel,在 dense 模型 + 高并发场景下已经能跑赢 vLLM。目前模型支持有限,但值得持续跟踪。
Google 今年 3 月发布的 TurboQuant 可以把 KV cache 压缩到 3 bit,显存占用降 6 倍,精度损失几乎为零。如果主流引擎集成了这个技术,70B 模型在 H100 上的并发能力会大幅提升。
FlashAttention-3 已经集成到了 vLLM 和 SGLang 里,在 Hopper 架构(H100)上提供了目前最快的 attention kernel。
以上就是三个主流推理引擎在 H100 上的实战对比。如果你也在做推理引擎选型,希望这些数据能帮到你。有问题可以评论区聊。