共计 4063 个字符,预计需要花费 11 分钟才能阅读完成。
很多朋友在本地服务器或者 NAS 上折腾大模型时,第一反应往往是拉起 Ollama 或者跑一段 HuggingFace 的原生 pipeline 脚本。单人测试时体验确实不错,一旦将接口接入内部系统、飞书机器人或 Dify 知识库,并发请求一上来,GPU 占用瞬间爆满,响应直接卡死,TTFT(Time To First Token,首字延迟)甚至飙到了十秒以上。这本质上是 KV Cache 碎片化管理与缺乏动态批处理机制造成的性能瓶颈。
为了解决团队内部多个 Agent 同时调用 LLM 导致的拥堵问题,我最近将私有云上的一台单卡 RTX 4090 服务器改造为基于 vLLM 架构的高并发推理网关。实测将 DeepSeek-R1-Distill-Qwen-14B 与 Qwen2.5-14B 的并发吞吐能力拉升了近 4 倍。这篇文章将实操步骤、核心启动参数调优以及踩坑细节完整记录下来,供大家直接抄作业。
一、为什么选 vLLM:摆脱 Ollama 的并发瓶颈
在严肃的业务场景中,推理框架的核心考核指标有两个:TTFT(首字响应时间)和Throughput(每秒处理 Token 数)。Ollama 底层基于 llama.cpp,对单请求的轻量部署极其友好,CPU/GPU 混合运行能力也强;但在多用户并发请求时,其显存复用和并发调度并不如专为服务端设计的推理引擎高效。
vLLM 的核心优势在于引入了 PagedAttention 机制。它模仿操作系统的虚拟内存分页,将连续的 KV Cache 切割为离散的物理块(Blocks),彻底解决了显存碎片化;配合其原生的 Continuous Batching(连续批处理),新进来的请求无需干等上一批任务全部生成完毕,可以在 Token 生成级别动态插入调度队列,从而让 GPU 核心计算单元始终保持跑满状态。
二、生产级 Docker 编排与核心参数压榨
不要在宿主机上肉身编译安装 PyTorch、CUDA 和 Triton,环境冲突会让人痛不欲生。直接采用官方 Docker 镜像并编写 docker-compose.yml 才是最稳妥且易于迁移的工程化做法。
以下是我在 RTX 4090(24GB 显存)环境下跑 Qwen2.5-14B-Instruct-AWQ 的标准配置。这里选择 AWQ 4-bit 量化版本,可以在保留 95% 以上精度的同时,腾出超过 12GB 显存给动态并发 KV Cache。
version: '3.8'
services:
vllm-engine:
image: vllm/vllm-openai:v0.6.3.post1
container_name: vllm-qwen-14b
runtime: nvidia
restart: always
environment:
- CUDA_VISIBLE_DEVICES=0
- NCCL_IGNORE_DISABLED_P2P=1
ports:
- "8000:8000"
volumes:
- /data/huggingface/hub:/root/.cache/huggingface/hub
ipc: host
command:
- "--model=Qwen/Qwen2.5-14B-Instruct-AWQ"
- "--quantization=awq"
- "--dtype=half"
- "--gpu-memory-utilization=0.92"
- "--max-model-len=8192"
- "--max-num-seqs=64"
- "--enforce-eager"
- "--trust-remote-code"
- "--port=8000"
核心参数避坑要点:
--gpu-memory-utilization=0.92:默认是 0.9。在确定没有其他进程抢显存的情况下,直接调到 0.92~0.94,能多榨出几百个上下文块。留 6-8% 给 CUDA runtime,避免分配器 OOM 崩掉容器。--max-model-len=8192:不要盲目开到模型上限的 32k 甚至 128k。对于 90% 的业务场景(如对话、代码补全),8192 足够用了。上限开得越高,预分配的每个序列最大显存占用估算就越大,导致可并发的请求槽位数(Slots)骤降。--max-num-seqs=64:限制最大连续批处理序列数。单卡跑 14B 模型,设为 64 是延迟与吞吐的最佳甜点位;设太大易导致首字延迟暴增。--enforce-eager:对于消费级显卡(如 3090/4090),强烈建议开启该参数关闭 CUDA Graph 捕获。虽然轻微损失一丁点理论性能,但能省下大约 1.5GB~2GB 显存,有效杜绝由于 Graph 预热导致的随机爆显存崩溃。ipc: host:千万别漏!PyTorch 多进程通信共享内存严重依赖这个设置,否则会出现高并发下进程死锁(Deadlock)并假死。
三、构建高并发流式反向代理(FastAPI + SSE)
在多数架构中,我们不会把 vLLM 端口直接暴露给公网或前端,而是需要经过一层业务网关,负责 API Key 校验、请求限流、敏感词审计以及长连接保活(Keep-Alive)。编写流式转发网关时,很多初学者会写成阻塞式等待,直接废掉了整个异步管道。
下面是一个使用 Python httpx 与 FastAPI 实现的原生 SSE(Server-Sent Events)异步透明流式转发微服务示例:
from fastapi import FastAPI, Request, HTTPException
from fastapi.responses import StreamingResponse
import httpx
import logging
app = FastAPI(title="LLM API Gateway")
VLLM_INTERNAL_URL = "http://127.0.0.1:8000/v1/chat/completions"
# 配置长连接连接池,防止高并发下频繁握手耗尽文件句柄
client = httpx.AsyncClient(timeout=httpx.Timeout(connect=5.0, read=60.0, write=5.0, pool=10.0),
limits=httpx.Limits(max_keepalive_connections=100, max_connections=200)
)
@app.post("/v1/chat/completions")
async def chat_proxy(request: Request):
headers = dict(request.headers)
headers.pop("host", None)
headers.pop("content-length", None)
try:
req_data = await request.json()
except Exception:
raise HTTPException(status_code=400, detail="Invalid JSON body")
# 强制将 stream 设为 True 走流式,保护显存并提升客户端体感
req_data["stream"] = True
async def event_generator():
try:
req = client.build_request("POST", VLLM_INTERNAL_URL, json=req_data, headers=headers)
res = await client.send(req, stream=True)
async for chunk in res.aiter_raw():
yield chunk
except httpx.ReadTimeout:
yield b"data: {\"error\": \"Upstream timeout\"}\n\n"
except Exception as e:
yield f"data: {{\"error\": \"{str(e)}\"}}\n\n".encode("utf-8")
return StreamingResponse(event_generator(), media_type="text/event-stream")
四、实测压测数据与性能表现对比
部署完成后,我使用 Locust 编写测试用例,对原生 Ollama 与优化后的 vLLM 方案进行横向基准测试。输入 Prompt 长度约为 300 Token,限制输出 200 Token,进行 30 个并发用户的压力回放测试。
| 指标项目 | Ollama (llama.cpp) | vLLM (AWQ + PagedAttn) | 性能差异 |
|---|---|---|---|
| 平均 TTFT (首字延迟) | 4,280 ms | 480 ms | 下降约 88.7% |
| 系统总吞吐量 (Tokens/s) | 115 tokens/s | 442 tokens/s | 提升约 384% |
| 显存碎片率 | 高(连续内存申请频繁失败) | 极低(近乎 0 碎片) | 显著改善 |
| 高并发请求错误率 (30 并发) | 13.3%(超时或连接中断) | 0.0% | 零丢包 |
可以看到,在高并发压力下,由于连续批处理发挥作用,vLLM 的整体系统吞吐达到了 440+ tokens/s。虽然单个请求的生成速率会因排队调度有些许损耗,但服务整体几乎没有丢包,且首字输出极其平稳,业务系统不会因为某个长文档分析任务阻塞整个推理管道。
总结与避坑心得
将单卡推上生产环境,核心其实就是在 显存占用 、 并发上下文长度 与首字延迟 这三者之间找平衡:
- 别拿显卡当全功能服务器用:如果单卡只有 24GB 显存,别直接死磕 FP16 的 14B 模型,AWQ 或 GPTQ 4-bit 量化是在工程成本与效果之间唯一的破局点。
- 严控最大序列长度:
--max-model-len决定了你的 KV Cache 调度池有多大。在满足业务的前提下压低它,就能直接换来翻倍的并发槽位。 - 宿主机文件句柄调优:很多人排查半天发现并发上不去,最后查出来是 Linux 默认
ulimit -n 1024把 SSE 长连接全断了。部署前务必把宿主机和 Docker 的nofile提到 65535。
按这套方案搭建的模型服务,不管是挂载在个人 NAS/ 工作站上为内网提供稳定算力,还是扔进轻量生产集群跑内部私有业务,都能在极其有限的硬件条件下榨出最高的产出比。