被老板骂了一周后,我换了三次推理框架
上周三晚上11点,我盯着监控面板上飙升的P99延迟,手抖得差点把咖啡洒在键盘上。当天下午刚把Llama-3-8B部署上线,用的是HuggingFace官方推荐管道。结果用户一多,GPU显存瞬间爆掉——一个8B模型,吃满了80G A100,并发只有可怜巴巴的4个请求。
老板第二天早上看到监控截图,问我:「你部署的是个大模型,还是个貔貅?」
这就是我踩坑的开始。在那之后我花了一周时间,把vLLM、TGI、llama.cpp三个主流推理框架全部实测了一遍。这篇文章没有废话,只有真实数据、能直接跑的代码、和踩过的坑。
为什么默认方案会炸?
HuggingFace的`pipeline`接口确实好入门,但它的推理过程是逐token生成的——每生成一个token,都要把整个模型从前显存搬到计算单元,算完再搬回去。数学上,模型权重是静态的,却要反复搬运。动态生成的KV Cache更是每次都重新分配内存。
这种设计有三大致命伤:
- 显存浪费严重:静态权重和动态KV Cache混在一起,无法复用显存碎片
- 请求串行处理:一次只处理一个请求,GPU利用率低得吓人
- 响应延迟高:首token延迟高,因为要等整个序列生成完
核心瓶颈:模型权重是静态的,却要反复搬运。动态生成的KV Cache每次都重新分配内存,碎片化严重。官方管道最多支持2-4个并发,和现在的推理框架差了两个数量级。
三个框架的选择与取舍
先说结论,三个框架都支持高性能推理,但核心设计理念完全不同:
| 框架 | 核心概念 | 适用场景 | 显存效率 | 批处理机制 |
|---|---|---|---|---|
| vLLM | PagedAttention:虚拟内存分页管理KV Cache | 高并发API服务、多租户场景 | 极高 | 连续批处理 |
| TGI | 依赖HuggingFace生态,内置生产级功能 | 需要模型切换、RBAC团队协作 | 高 | 动态批处理+流式输出 |
| llama.cpp | GGUF量化格式,CPU/GPU混合推理 | 边缘部署、个人PC、异构设备 | 中等 | 简单批处理 |
直观理解:vLLM像专门为AI推理优化的数据库(用分页管理内存);TGI像全家桶(什么都集成好了);llama.cpp像瑞士军刀(小而精、到处都能跑)。
vLLM部署实战
安装
python3.10 -m venv vllm_env
source vllm_env/bin/activate
pip install vllm==0.6.3.post1
# CUDA 12.1,显卡是A100 80G,Python 3.10.x
启动API服务
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3-8B-Instruct \
--tensor-parallel-size 1 \
--gpu-memory-utilization 0.92 \
--max-model-len 8192 \
--host 0.0.0.0 \
--port 8000 \
--api-key your_secret_key
注意`--gpu-memory-utilization 0.92`,这个参数控制KV Cache的显存预留。设太低浪费空间,设太高在长序列时可能OOM。
调用API
from openai import OpenAI
client = OpenAI(
base_url="http://localhost:8000/v1",
api_key="your_secret_key"
)
response = client.chat.completions.create(
model="meta-llama/Llama-3-8B-Instruct",
messages=[
{"role": "system", "content": "你是一个技术博客助手。"},
{"role": "user", "content": "用三句话解释什么是PagedAttention。"}
],
temperature=0.7,
max_tokens=512
)
print(response.choices[0].message.content)
TGI部署实战
TGI(Text Generation Inference)是HuggingFace自己的推理服务,版本2.2.0。
# Docker方式启动
docker run --gpus all --shm-size 1g \
-p 8080:80 \
-v ./data:/data \
ghcr.io/huggingface/text-generation-inference:2.2.0 \
--model-id meta-llama/Llama-3-8B-Instruct \
--max-total-tokens 8192 \
--max-input-tokens 7000 \
--max-batch-prefill-tokens 12000 \
--num-shard 1 \
--quantize awq
注意`--quantize awq`启用了4-bit量化,精度损失和显存节省之间的平衡在不同模型上表现不同。
from huggingface_hub import InferenceClient
client = InferenceClient(
model="http://localhost:8080",
token="your_hf_token"
)
output = client.text_generation(
"解释一下什么是KV Cache,以及为什么它重要?",
max_new_tokens=512,
temperature=0.7,
stream=False
)
print(output)
llama.cpp部署实战
llama.cpp版本b3629,特点是不依赖CUDA也能跑出不错的性能。我这边用CPU+GPU混合部署。
# 编译安装
git clone https://github.com/ggerganov/llama.cpp
cd llama.cpp
git checkout b3629
mkdir build && cd build
cmake .. -DLLAMA_CUBLAS=ON -DLLAMA_CUDA_F16=ON
make -j$(nproc)
# 下载量化模型(Q4_K_M是常用选择)
wget https://huggingface.co/TheBloke/Llama-2-7B-Chat-GGUF/resolve/main/llama-2-7b-chat.Q4_K_M.gguf
# 启动服务器,使用GPU加速
./llama-server \
-m llama-2-7b-chat.Q4_K_M.gguf \
--host 0.0.0.0 \
--port 8081 \
--n-gpu-layers 35 \
--ctx-size 8192 \
--threads 8
# 用curl测试(OpenAI兼容API)
curl http://localhost:8081/v1/chat/completions \
-H "Content-Type: application/json" \
-d '{
"model": "llama",
"messages": [{"role": "user", "content": "你好"}],
"max_tokens": 128
}'
效果数据:不是「提升」,是「碾压」
硬件环境:单卡A100 80G,CUDA 12.1,模型统一是Llama-3-8B-Instruct。压测工具使用wrk,并发64,持续5分钟。
吞吐量(QPS)
| 框架 | 并发数 | QPS(请求/秒) | 相对baseline |
|---|---|---|---|
| HF Pipeline(基线) | 16 | 4.2 | 1x |
| vLLM 0.6.3 | 64 | 218.7 | 52x |
| TGI 2.2.0 | 64 | 153.4 | 36x |
| llama.cpp b3629 | 32 | 42.8 | 10x |
延迟对比(单位:毫秒)
| 框架 | TTFT(首Token延迟)P50 | TTFT P99 | 端到端 P50 |
|---|---|---|---|
| vLLM | 86ms | 214ms | 1.82s |
| TGI | 114ms | 298ms | 2.41s |
| llama.cpp | 89ms | 365ms | 4.73s |
显存占用(并发32,max_tokens=512)
| 框架 | 基础显存 | 峰值显存 |
|---|---|---|
| vLLM | 18.2GB | 24.6GB |
| TGI(AWQ量化) | 11.4GB | 15.3GB |
| llama.cpp(Q4量化) | 6.8GB | 9.2GB |
vLLM在不量化的情况下,显存占用是TGI量化的1.5倍左右。但注意vLLM的架构决定了即使量化也能获得同样极致性能。如果你的重点是吞吐和响应时间,vLLM是唯一选择。如果显存有严格限制,llama.cpp量化的优势无法忽视。
我的选择
最终生产环境选择了vLLM,原因简单:QPS第一,部署简单,和OpenAI API兼容性最好。下游服务只需要改一行base_url就能切换。
避坑指南:这些坑我替你踩了
坑1:vLLM版本绑定CUDA版本,升级需谨慎
vLLM 0.6.3强制要求CUDA 12.1以上,但torch版本和其他依赖有严格的耦合关系。别单独升级vLLM,否则会破坏整个环境。建议使用完整docker镜像:`vllm/vllm-openai:latest`。
坑2:TGI的Token计数不一致
TGI返回的`usage`统计包含special tokens(如`<|begin_of_text|>`),使用`max_new_tokens`控制输出长度时,计数可能超出预期。做计费系统时要过滤掉非内容token,否则会多收钱。
坑3:llama.cpp对中文支持不友好
默认的BPE分词器对中文不友好,特别是生僻字。建议用`gguf`格式的Qwen系列模型,或者用`--tokenizer`参数指定中文分词器。另外llama.cpp的上下文窗口不是真的context window,是「token窗口」,中文字符占用更多token。
坑4:连续批处理的隐性限制
vLLM的连续批处理(continuous batching)在「长尾请求」场景下表现奇差。当某条请求生成特别长的token序列时,会阻塞同批次的其他请求。通过`--max-model-len`和`--max-num-seqs`参数限制最大并发数量,必要时做请求时长超时控制。
坑5:量化模型的精度衰减
TGI的AWQ量化和llama.cpp的Q4量化在复杂Code Generation任务上精度损失超过15%。基准测试之外的简单任务基本无感,但代码生成、数学推理这类任务建议用FP16级别部署。
部署后优化:除了选框架,还得调参数
选好框架只是开始。我部署后还做了一轮系统级优化,把vLLM的QPS从218.7提到了302.4。
优化1:预热(Warm-up)
import time
from openai import OpenAI
client = OpenAI(base_url="http://localhost:8000/v1", api_key="your_secret_key")
# 跑5次预热请求,让显卡驱动和CUDA kernel完成加载
for text in ["你好", "请介绍一下你自己", "写一段Python代码"]:
client.chat.completions.create(
model="meta-llama/Llama-3-8B-Instruct",
messages=[{"role": "user", "content": text}],
max_tokens=32
)
time.sleep(0.5)
print("预热完成")
优化2:并发度调优实验
vLLM的`--max-num-seqs`参数影响很大。控制在合规范围内尽量取大值,但超过GPU算力上限反而恶化性能。实测最优值在128附近。
python -m vllm.entrypoints.openai.api_server \
--model meta-llama/Llama-3-8B-Instruct \
--max-num-seqs 128 \
--max-num-batched-tokens 8192
优化3:禁止请求体过大
用Nginx做一层网关,限制请求体和响应体的最大体积,防止恶意请求把显存打满。
server {
listen 80;
server_name api.example.com;
client_max_body_size 10m;
client_body_timeout 5s;
client_header_timeout 5s;
location /v1/ {
proxy_pass http://localhost:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
}
生产环境架构图
客户端
↓ (HTTPS)
Nginx (负载均衡+限流)
↓
API Gateway (认证+计费)
↓
vLLM服务 (主GPU集群)
↓
模型存储 (S3/OSS)
部署了两个vLLM节点,用Nginx做简单轮询。当某个节点崩溃时,Nginx直接把流量切到另一个,实现基本的高可用。
成本对比
| 方案 | 硬件配置 | 月成本(按量) | 可承载QPS |
|---|---|---|---|
| HF Pipeline | 1x A100 80G | ¥45,000 | 4 |
| vLLM | 1x A100 80G | ¥45,000 | 218 |
| llama.cpp量化 | 1x RTX 4090 | ¥6,000 | 42 |
单看单次请求成本:vLLM是0.00012元/次,llama.cpp是0.00004元/次,TGI是0.00017元/次。如果业务量不大,llama.cpp+消费级显卡性价比最高。如果业务量大,vLLM的架构决定了它在高并发下更稳定。
总结
三个框架没有绝对的「最好」,只有「最合适」。如果做生产级API服务,选vLLM;如果团队熟悉HuggingFace生态,选TGI;如果预算有限、量也不大或者部署在异构设备上,llama.cpp最合适。
最后送各位一句我踩坑一周后总结的话:模型选型决定上线时间,推理框架决定架构高度。