从显存OOM到千Token吞吐:单机部署vLLM与LiteLLM并发推理调优避坑实战

2次阅读
没有评论

共计 3504 个字符,预计需要花费 9 分钟才能阅读完成。

上个月给团队搭建内部代码补全和多智能体协同后台,硬件给到了一台双路 RTX 4090(24GB x 2)的 Ubuntu 工作站。刚开始为了图省事,直接跑了 Ollama 挂载 Qwen2.5-32B-Instruct。原本以为 8 并发以内足够应付,结果并发一拉到 12,首 Token 延迟(TTFT)直接从 300ms 飙到 5 秒开外,甚至数次触发 CUDA OOM 导致进程被 systemd 无情 SIGKILL。

对于个人极客或微型团队来说,消费级显卡的显存带宽和容量极度宝贵。要想在有限显卡资源下吃满并发,必须抛弃黑盒封装过度的方案。本文记录我将本地推理栈全面重构成 vLLM + LiteLLM 统一网关 的实战调优过程,包含真实的踩坑参数、Nginx 流式代理缓冲陷阱以及压测对比数据。

一、架构选型痛点:Ollama 为什么扛不住高并发?

Ollama 底层基于 llama.cpp,在 Mac 本地单人单任务推理时体验极佳,但在多用户并发请求、长上下文(Context Length > 8k)场景下有几个难以规避的短板:

  • 内存碎片与固定 KV 预分配: 并发多任务下,静态分配显存会导致严重的内部与外部碎片,导致显存利用率看起来很高,实际计算单元却在空转等待。
  • 批处理(Continuous Batching)调度能力有限: 当不同请求的 Prompt 长度和生成长度差异巨大时,调度器无法高效动态交错生成,短请求会被长请求卡死。
  • 缺乏工业级网关治理: 没有原生提供完善的多模型负载均衡、虚拟 API Key 配额管控以及细粒度的降级策略。

因此,工业级部署的标配是:底座采用基于 PagedAttention 的 vLLM 负责模型并发推理与显存精细化管理,上层接入轻量级的 LiteLLM Proxy 充当 API 网关,提供鉴权、负载均衡、重试与 OpenAI 标准协议兼容。

二、vLLM 底座部署与核心参数“黄金配比”

在双卡 4090 上跑 Qwen2.5-32B(推荐 AWQ 4-bit 量化版),显存总量为 48GB,模型权重占用约 19.5GB,剩余显存几乎全部需要划拨给 KV Cache。启动脚本绝不能用默认参数裸跑,必须做精细限制。

1. 核心启动脚本(systemd 守护进程或 Docker)

以下是我线上跑通的生产级启动参数组合(模型路径请替换为你本地存储路径):

python3 -m vllm.entrypoints.openai.api_server \
    --model /data/models/Qwen2.5-32B-Instruct-AWQ \
    --tensor-parallel-size 2 \
    --dtype float16 \
    --gpu-memory-utilization 0.92 \
    --max-model-len 16384 \
    --max-num-seqs 64 \
    --kv-cache-dtype auto \
    --swap-space 16 \
    --trust-remote-code \
    --port 8000 \
    --host 0.0.0.0 \
    --disable-log-requests

2. 关键参数排障与避坑解析

  • --tensor-parallel-size 2:双卡 4090 走张量并行。 踩坑提示: 消费级显卡没有 NVLink,走 PCIe 4.0 4x/8x 互联时 NCCL 通信可能成为瓶颈。如果运行报 P2P 错误,必须在环境变量中补上一句 export NCCL_P2P_DISABLE=1,强制走共享内存或系统内存通信。
  • --gpu-memory-utilization 0.92:默认 0.90。实测设置到 0.95 时,如果在高并发瞬间遭遇长文本突发,极易因 CUDA 内部工作缓存分配失败而 Crash;保留 8%(约 3.8GB)余量最为稳健。
  • --max-model-len 16384:务必按实际业务截断。Qwen2.5 官方支持 32k/128k,但 KV Cache 显存消耗与上下文长度呈线性正比。如果开到 32k,留给并发槽位的数量会被大幅压缩,导致稍有并发就触发排队甚至请求降级。
  • --max-num-seqs 64:限制调度器每批处理的最大并发请求数,避免单次迭代中调度过多序列造成 Attention 计算延迟剧增。
  • --disable-log-requests:高并发时实时输出每个 Request 的文本日志会带来不可忽视的 I/O 阻塞,生产环境务必关掉。

三、网关层接入:LiteLLM 动态路由与反代缓冲陷阱

直接把 8000 端口暴露给客户端极不安全,且缺乏并发队列缓冲和请求限流。架构第二层是在前置部署 LiteLLM,并配合 Nginx 进行统一代理。

1. LiteLLM 生产配置 (config.yaml)

model_list:
  - model_name: qwen-32b
    litellm_params:
      model: openai/Qwen2.5-32B-Instruct-AWQ
      api_base: http://127.0.0.1:8000/v1
      api_key: "none"
      tpm: 120000
      rpm: 120

general_settings:
  master_key: "sk-nassky-prod-master-key-xyz"
  store_model_in_db: false

litellm_settings:
  num_retries: 3
  request_timeout: 60
  stream_chunk_timeout: 5.0

2. 踩坑最深处:Nginx 反向代理流式传输“卡顿吐字”

在调试 SSE(Server-Sent Events)流式输出时,开发人员反馈光标总是不动,然后隔几秒“哗啦”一下子蹦出一整段字,毫无流式体验。排查发现是 Nginx 默认启用了 Proxy Buffering。

在 Nginx 的 location 块中必须强制禁用缓冲并调整 Keep-Alive 超时:

location /v1/ {
    proxy_pass http://127.0.0.1:4000; # 转发到 LiteLLM
    proxy_http_version 1.1;
    proxy_set_header Connection "";
    proxy_set_header Host $host;
    proxy_set_header X-Real-IP $remote_addr;

    # 核心配置:彻底关闭反向代理缓冲,保持 SSE 管道畅通
    proxy_buffering off;
    proxy_cache off;
    chunked_transfer_encoding on;

    # 防止长提示词推理初始等待过长触发 504 网关超时
    proxy_read_timeout 600s;
    proxy_send_timeout 600s;
}

四、实测压测对比:Ollama vs vLLM 吞吐大比拼

使用 vllm/benchmarks/benchmark_serving.py 在同一台机器上对两套方案进行压力测试。模型均为 Qwen2.5-32B 4-bit 量化,输入 Prompt 平均长度约 1800 Token,输出限制 512 Token。

指标项 Ollama 方案 vLLM + LiteLLM 方案 改善幅度
并发请求数 (Concurrency) 16 16 –
整体吞吐 (Tokens/s) 112.4 438.7 +290%
首字延迟 P90 (TTFT) 4,820 ms 680 ms -85.8%
单 Token 生成延迟均值 (ITL) 42.5 ms 18.2 ms -57.1%
显存溢出发生率 (OOM) 偶发 (约 8.3%) 0% (无单次中断) 稳定性彻底解决

实测数据差异极其明显。PagedAttention 对显存的精细化物理分页(Block-level 分配)让显存几乎没有碎片,Continuous Batching 则保证了新请求到达时不需要等整个 Batch 的请求全部生成完毕,而是随时插入 Attention 算子运算,吞吐直接拉满翻倍。

总结与避坑心得

折腾这一套推理集群,最大的收获就是: 不要迷信默认开箱即用的封装工具 。对于单人学习体验,Ollama 依然是开箱即用的首选;但只要涉及到局域网服务、团队共享、Agent 链式调用(大量并发短时任务),底层换上 vLLM 是唯一能够最大化榨干显卡计算潜力的路径。

回顾部署要点,建议按如下 checklist 进行自检:

  1. 双卡消费级显卡(无 NVLink)务必注意 P2P 通信配置,必要时配置环境变量切断 NCCL P2P 规避内存死锁。
  2. 严格根据场景控制 --max-model-len,把宝贵的显存让位给并发 KV Cache 块,而不是保留用不到的超长无用空间。
  3. 上游网关和 Nginx 代理必须关闭 proxy_buffering,否则你的流式传输永远处于“便秘”状态。
  4. 前置 LiteLLM 做 API Token 隔离与 Rate Limit,防止个别客户端脚本写死无限循环直接把单机后端压穿。
正文完
 0
评论(没有评论)