共计 5465 个字符,预计需要花费 14 分钟才能阅读完成。
最近在折腾局域网多 Agent 编排项目时,我遇到了一个极其让人抓狂的瓶颈:前端 Dify 和自研的 Coding Agent 同时发起请求,一侧调用代码推理模型(Qwen2.5-Coder),另一侧频繁调用结构化提取小模型,显存直接被打满 OOM,流式响应的首字延迟(TTFT)一路飙升到 4 秒以上。如果不舍得堆显卡,单张消费级卡或入门级数据中心卡到底能不能撑起稳定、低延迟且支持多模型动态分流的本地网关?
答案是能,但必须干掉 Ollama 这种黑盒封装严重的方案。上周我花了两天时间,用 vLLM + LiteLLM 重新重构了家里的服务器与工作室的推理节点。跑了一圈并发压测,不仅把单卡利用率榨干到 95% 以上,首字响应还成功压到了 280ms 以内。今天把这套生产级可用的单机多模型 API 网关搭建、关键参数调优及踩坑记录完整倒出来。
一、架构设计:为什么放弃 Ollama 选 vLLM+LiteLLM 组合?
很多极客在本地折腾大模型时起手就是 ollama run,玩玩单轮对话很爽,但只要放到多客户端或稍微有点并发的场景,问题立刻暴露:
- KV Cache 管理粗糙: 并发请求一多,显存碎片化严重,并发吞吐断崖式下跌。
- 模型冷热切换不可控: Ollama 的内存驻留时间(keep_alive)机制不够细致,多模型交替请求时频繁 load/unload 模型权重,导致十几秒的卡顿。
- 缺少协议级分流与 Fallback: 客户端代码需要写一堆适配逻辑,既难做多端鉴权,又无法在本地显卡跑满时自动 fallback 降级到外部便宜的公有云 API(如 DeepSeek 或 DeepInfra)。
这套方案的核心架构是:底座采用 vLLM,利用其 PagedAttention 机制彻底消灭显存碎片,通过显存配额锁死基础推理池;前端反向代理采用 LiteLLM,对外提供完全兼容 OpenAI 标准的统一 Endpoint,负责动态负载均衡、流式缓冲切片、超时熔断与 API 密钥配额管理。
二、底层引擎:vLLM 双实例精确显存切分实战
在单张 24G 显存(如 RTX 3090/4090)的机器上,要同时常驻一个代码大模型(14B INT4/AWQ)和一个高频小模型(7B/8B AWQ),直接无脑启动两个 vLLM 实例必定直接爆显存。这是因为 vLLM 默认会贪婪地抢占 gpu_memory_utilization = 0.90 的显存。
我们需要通过精确限制显存占用与最大上下文窗口来实现双模型常驻。以下是我调试通过的多容器编排片段:
# docker-compose.vllm.yml
version: '3.8'
services:
# 实例 A:主力代码与推理模型
vllm-coder:
image: vllm/vllm-openai:latest
container_name: vllm-coder-14b
runtime: nvidia
environment:
- CUDA_VISIBLE_DEVICES=0
volumes:
- /mnt/storage/hf_cache:/root/.cache/huggingface
ports:
- "8001:8000"
ipc: host
command: >
--model Qwen/Qwen2.5-Coder-14B-Instruct-AWQ
--quantization awq
--port 8000
--max-model-len 8192
--gpu-memory-utilization 0.65
--swap-space 4
--enforce-eager
--disable-log-requests
restart: unless-stopped
# 实例 B:高频任务与轻量 Agent 提取模型
vllm-chat:
image: vllm/vllm-openai:latest
container_name: vllm-chat-7b
runtime: nvidia
environment:
- CUDA_VISIBLE_DEVICES=0
volumes:
- /mnt/storage/hf_cache:/root/.cache/huggingface
ports:
- "8002:8000"
ipc: host
command: >
--model Qwen/Qwen2.5-7B-Instruct-AWQ
--quantization awq
--port 8000
--max-model-len 4096
--gpu-memory-utilization 0.28
--swap-space 2
--enforce-eager
--disable-log-requests
restart: unless-stopped
这里踩过的两个深坑必须提醒:
第一,--gpu-memory-utilization 的加和切勿刚好等于 1.0。 在上面的配置中,0.65 + 0.28 = 0.93,留了整整 7%(约 1.6GB)作为 CUDA Context 和 PyTorch 运行时额外缓冲。如果拉得太满,CUDA 初始化时就会直接抛出 CUDA out of memory。
第二,启用 --enforce-eager 避免 CUDA Graph 预捕获挤爆显存。 默认情况下 vLLM 会开启 CUDA Graph 来加速小 batch 的推理,但这会在初始化时额外吃掉约 1~2GB 显存。对于单卡多实例场景,加上 --enforce-eager 虽然会损失大概 3%~5% 的极致吞吐,但能极大降低显存硬占用,保证两个实例顺利启动。
三、中间件代理:LiteLLM 统一分发与降级熔断
后端两个 vLLM 实例分别挂在宿主机的 8001 和 8002 端口。接下来需要 LiteLLM 接管所有的入站流量。在根目录下创建 litellm_config.yaml:
model_list:
# 别名映射:代码补全主力路由
- model_name: coder-default
litellm_params:
model: openai/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ
api_base: http://host.docker.internal:8001/v1
api_key: "EMPTY"
rpm: 120
timeout: 60
# 极速轻量路由
- model_name: fast-chat
litellm_params:
model: openai/Qwen/Qwen2.5-7B-Instruct-AWQ
api_base: http://host.docker.internal:8002/v1
api_key: "EMPTY"
rpm: 300
# 混合路由策略:当本地大模型排队超时或宕机时,自动无缝降级到云端
- model_name: production-deepseek
litellm_params:
model: openai/Qwen/Qwen2.5-Coder-14B-Instruct-AWQ
api_base: http://host.docker.internal:8001/v1
api_key: "EMPTY"
- model_name: production-deepseek
litellm_params:
model: deepseek/deepseek-chat
api_key: os.environ/DEEPSEEK_API_KEY
router_settings:
routing_strategy: "latency-based-routing"
num_retries: 2
timeout: 30
allowed_fails: 2
cooldown_time: 15
general_settings:
master_key: "sk-nassky-local-gateway-secure-token"
紧接着用极简容器启动 LiteLLM 反向代理网关:
docker run -d \
--name litellm-gateway \
-p 4000:4000 \
-v $(pwd)/litellm_config.yaml:/app/config.yaml \
-e DEEPSEEK_API_KEY="sk-your-backup-key" \
--extra-hosts=host.docker.internal:host-gateway \
ghcr.io/berriai/litellm:main-latest \
--config /app/config.yaml --port 4000
现在,全内网的沉浸式翻译插件、VSCode 里的 Continue 插件、Dify 应用只要将 API Base 设为 http://192.168.x.x:4000/v1,输入密钥 sk-nassky-local-gateway-secure-token,即可无缝使用。只要请求 coder-default,流量就走本地 14B;请求 production-deepseek 时,一旦本地显存告急拒绝服务,LiteLLM 会自动在 30 秒超时后无感转给公有云 API,彻底保障了业务高可用。
四、极致压测:流式输出与 TTFT 性能实测
配置做完了,性能到底提升了多少?我写了一个异步并发脚本,模拟 8 个客户端并发向网关发起长提示词(约 1500 tokens input)并流式接收生成结果,对比原生 Ollama 多模型切换方案与这套 vLLM+LiteLLM 方案的指标表现:
import asyncio
import time
import httpx
API_URL = "http://localhost:4000/v1/chat/completions"
HEADERS = {"Authorization": "Bearer sk-nassky-local-gateway-secure-token"}
PAYLOAD = {
"model": "fast-chat",
"messages": [{"role": "system", "content": "You are a professional system architect."},
{"role": "user", "content": "Write a distributed rate limiter in Rust using Token Bucket algorithm."}
],
"stream": True
}
async def benchmark_client(client_id):
async with httpx.AsyncClient(timeout=60.0) as client:
start_time = time.perf_counter()
first_token_time = None
total_tokens = 0
async with client.stream("POST", API_URL, headers=HEADERS, json=PAYLOAD) as response:
async for chunk in response.aiter_lines():
if chunk.startswith("data:") and chunk != "data: [DONE]":
if first_token_time is None:
first_token_time = time.perf_counter() - start_time
total_tokens += 1
total_time = time.perf_counter() - start_time
print(f"[Client {client_id}] TTFT: {first_token_time*1000:.1f}ms |"
f"Tokens: {total_tokens} | TPS: {total_tokens/total_time:.2f} tok/s")
async def main():
tasks = [benchmark_client(i) for i in range(8)]
await asyncio.gather(*tasks)
if __name__ == "__main__":
asyncio.run(main())
实测数据汇总(单卡 RTX 4090 24G,测试场景:8 并发长文本流式生成):
| 对比维度 | Ollama 默认并发架构 | vLLM 双实例 + LiteLLM 调优方案 | 提升幅度 |
|---|---|---|---|
| 冷热模型交替延迟 | 8.4s ~ 14.2s (伴随 Reload) | < 350ms (双实例常驻内存) | 延迟降低 95%+ |
| 平均 TTFT (首字延迟) | 3120ms | 278ms | 响应速度提升 11 倍 |
| 总吞吐量 (Aggregate TPS) | 34.2 tokens/s | 148.6 tokens/s | 吞吐提升 334% |
| 显存溢出率 (OOM Rate) | 12.5% (并发尖峰丢包) | 0% (受 PagedAttention 与配额保护) | 零故障 |
总结与避坑心得
折腾大模型推理架构不是显卡参数的简单堆砌,更多是在显存分配、并发队列和网关路由层面的精细化权衡。总结几条实操心得:
- 别迷信 FP16 原生精度: 在边缘或单卡场景下,AWQ 和 GPTQ 量化在降低 70% 显存占用的同时,几乎没有可以感知的语义损失,还能换来极大的上下文空间(8K+)。
- 分清计算密集与显存带宽密集: 对于首字延迟极度敏感的应用(如代码实时自动补全),用独立实例跑精简小模型,哪怕模型参数只有 7B,体验也远远好过排队等待 14B/32B 模型。
- 用好 Gateway 层: 千万不要让前端直接裸连 vLLM。LiteLLM 不仅提供了重试降级与熔断防护,它内置的日志和 Token 计量功能,在小团队和个人多客户端场景下也是极佳的排障工具。
按本文这套拓扑搭建,你手头闲置的单张 4090 或者云上按小时租用的计算卡,就能真正承接起高可用、多模型并发的自建 AI 生产力网关,彻底告别 OOM 与卡顿。
===END===