共计 3998 个字符,预计需要花费 10 分钟才能阅读完成。
很多朋友在本地服务器或 NAS 配了一张 24G 显存的 RTX 4090,跑 Ollama 单人聊天确实很顺畅。但只要接入沉浸式翻译、IDE 代码补全或者开放给团队几个人同时跑 Agent 工作流,接口就会迅速卡死:首字延迟(TTFT)从 200ms 暴增至十几秒,并发请求直接排队超时,甚至在并发突增时直接抛出显存分配失败错误。单任务推理与高并发服务的工程架构,本质上完全是两码事。
一、为什么本地多并发必须抛弃原生 Transformers?
用原生的 HuggingFace Transformers 配合 FastAPI 跑并发推理,瓶颈从来不在显卡的计算核心(Tensor Core),而是在 内存带宽 与KV Cache 显存碎片。大模型自回归生成的机制决定了每个 Token 都要读写前面所有上下文生成的 Key 和 Value 缓存。
- 内存浪费与连续空间分配:原生框架必须预先为请求分配连续的显存空间(按最大上下文长度分配,例如 4096 或 8192)。哪怕用户只输入了 10 个字,大量显存也只能空置挂着,单卡跑 3 个并发就直接 OOM。
- 首字计算与生成计算争抢:新请求进入时的预填充(Prefill,计算密集型)会无情阻断正在生成(Decode,显存带宽密集型)的流式输出,导致已经连接的客户端出现严重的卡顿与断流。
在生产级架构中,解决这个问题的杀手锏是vLLM。它引入了操作系统的分页内存机制(PagedAttention),将 KV Cache 切分成不连续的物理块,显存利用率直接从不足 40% 提升到 90% 以上。
二、架构设计:vLLM + FastAPI 异步网关 + Nginx SSE 流式透传
直接将 vLLM 的 8000 端口暴露给业务层不是明智之举。生产环境中我们需要一个三层架构:前置 Nginx 负责 SSL 终止、长连接复用与缓冲禁用,中间层 FastAPI 做鉴权、Token 用量统计与动态排队,底层 vLLM 负责纯粹的高性能推理计算。
1. 启动 vLLM 后端引擎
以 Qwen2.5-14B-Instruct-AWQ 量化模型为例,单卡 24G 显卡完全可以吃下 14B 的 4bit 模型,并预留足够的显存作为 KV Cache 池:
# 启动 vLLM 推理引擎,限制显存占用并启用分块预填充
vllm serve Qwen/Qwen2.5-14B-Instruct-AWQ \
--host 127.0.0.1 \
--port 8000 \
--gpu-memory-utilization 0.90 \
--max-model-len 8192 \
--max-num-seqs 64 \
--enable-chunked-prefill \
--trust-remote-code
关键参数避坑说明:
--gpu-memory-utilization 0.90:显存利用率上限。切勿设为 1.0,必须为 CUDA Context 和 PyTorch 临时计算预留 5%~10% 的缓冲空间。--enable-chunked-prefill:极为关键的低延迟开关。它会将长输入的 Prefill 阶段拆解为多个分块,穿插在 Decode 生成阶段之间,彻底消除了新请求进入导致其他并发流卡死的问题。--max-num-seqs 64:允许并发处理的最大序列数,结合显存大小动态约束。
2. 异步流式代理网关(Python / FastAPI)
使用 httpx.AsyncClient 实现真正的非阻塞异步流转发。这里有一个深坑:如果客户端中途断开连接,后端的推理必须立即取消,否则显卡会持续空转浪费算力。
import httpx
from fastapi import FastAPI, Request
from fastapi.responses import StreamingResponse
app = FastAPI()
VLLM_API_URL = "http://127.0.0.1:8000/v1/chat/completions"
# 维持持久连接池,避免每次请求频繁进行 TCP 握手
http_client = httpx.AsyncClient(timeout=httpx.Timeout(60.0, connect=5.0),
limits=httpx.Limits(max_keepalive_connections=50, max_connections=200)
)
@app.post("/v1/chat/completions")
async def proxy_chat(request: Request):
body = await request.json()
body["stream"] = True # 强制流式输出
async def stream_generator():
try:
req = http_client.build_request("POST", VLLM_API_URL, json=body)
res = await http_client.send(req, stream=True)
async for chunk in res.aiter_bytes():
# 检查客户端是否已经断开连接
if await request.is_disconnected():
await res.aclose()
break
yield chunk
except Exception as e:
yield f"data: {{\"error\": \"{str(e)}\"}}\n\n".encode("utf-8")
return StreamingResponse(stream_generator(),
media_type="text/event-stream",
headers={
"Cache-Control": "no-cache",
"Connection": "keep-alive",
"X-Accel-Buffering": "no" # 通知 Nginx 不要做缓冲区暂存
}
)
3. Nginx 核心反向代理配置
流式响应最常见的问题是文字不是“吐出来”的,而是等了三四秒突然“喷出一大段”。这通常是 Nginx 开启了 proxy_buffering 导致的。修改你的 Nginx 站点配置:
upstream agent_backend {
server 127.0.0.1:8080; # FastAPI 服务端口
keepalive 32;
}
server {
listen 80;
server_name api.lan.nassky.top;
location /v1/ {
proxy_pass http://agent_backend;
proxy_http_version 1.1;
proxy_set_header Connection "";
# 核心:关闭反向代理缓冲区,实现打字机效果
proxy_buffering off;
proxy_cache off;
chunked_transfer_encoding on;
# 长连接超时时间,针对长文本生成场景
proxy_read_timeout 300s;
proxy_send_timeout 300s;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
三、实测压测数据对比
我在单张 RTX 4090 24GB 上,对 Qwen2.5-14B-Instruct-AWQ 进行了压测对比。测试场景为:提示词输入 512 tokens,生成输出 256 tokens,使用 wrk 与自定义并发测试脚本循环打入 32 个并发流:
| 方案架构 | 首字延迟 (TTFT P95) | 系统总吞吐量 (tokens/s) | 显存占用率 | 并发错误率 (32 并发) |
|---|---|---|---|---|
| Ollama 默认部署 | 4,820 ms | 184.2 | 22.8 GB | 12.5% (显存排队超时) |
| vLLM 基础配置 | 640 ms | 612.8 | 21.6 GB | 0% |
| vLLM + Chunked Prefill + 异步代理 | 210 ms | 785.4 | 21.6 GB | 0% |
实测数据非常明显:开启 Chunked Prefill 后,首字延迟直接从几秒压到了 200 毫秒出头,几乎消除了突发请求带来的抖动。吞吐量相比原生部署提升了 4 倍以上,24G 显存被压榨到了极限但没有发生溢出。
四、踩坑排障实录
1. 显存碎片导致偶发 CUDA OOM
现象:服务刚启动正常,运行几个小时后突然报torch.cuda.OutOfMemoryError。
排查与解决:这是因为 Linux 系统的共享内存(shm)或 PyTorch 本身的显存池碎片未释放。如果是在 Docker 中运行,启动容器时 必须显式增加 shm 大小:--shm-size="16gb"。同时在容器环境变量中加入export PYTORCH_CUDA_ALLOC_CONF=expandable_segments:True,让 PyTorch 动态扩展段而不是强求连续内存。
2. 客户端取消请求后显卡空转
现象:用户网页刷新了,后台显卡依然在呼呼狂转十几秒生成剩下的 Token。
解决:务必在 FastAPI 代理中监听request.is_disconnected(),一旦断开立刻向 vLLM 的流连接发送 Abort 信号。vLLM 本身具备请求中止的 RPC 通知能力,中断后会立刻回收该序列占用的 KV Cache 块。
总结与部署建议
在个人工作站或 NAS 私有云环境中跑 AI Agent,别再用裸奔的 Python 脚本启动了。将推理核心与网络网关解耦,使用vLLM 处理 PagedAttention 与批处理调配,上层配合FastAPI + Nginx 流式透传,不仅能够让你的单卡显存利用率最大化,更能为多 Agent 协作提供毫秒级响应的高可靠 API 服务。
如果手头显存极度紧张(例如 16G 显存跑 14B 量化),可以适当调低 --max-model-len 至 4096,并调整 gpu_memory_utilization 为 0.85,优先保障系统进程与显存回收的稳定性。
===EOF===