告别 OOM 与断流:vLLM + Caddy 搭建生产级高并发流式推理网关实战

6次阅读
没有评论

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

很多开发者在单卡机器上用 Ollama 跑 Qwen2.5 或者 DeepSeek-R1-Distill 系列模型感觉很流畅,但一旦接入业务前端或多 Agent 工作流,并发请求上到 20 以上时,服务立刻崩溃:要么显存直接 OOM 崩溃退出,要么首字延迟(TTFT)飙升到 10 秒开外,甚至前端的 SSE(Server-Sent Events)长连接频繁被网关超时切断。

Ollama 适合本地调试,但它的并发调度粒度较粗,显存动态管理能力相对有限。要在有限显存(比如两块消费级 RTX 4090 24G 或单张 A100/H800)上撑起稳定 50+ 甚至上百的流式推理并发,必须转入工业级方案。这篇文章是我在给团队搭建私有推理集群时踩平无数坑后沉淀的实战架构:vLLM 异步推理引擎 + Caddy 流式反向代理。

一、为什么是 vLLM?并发底层的两个核心机制

解决大模型高并发,核心矛盾在于显存中的 KV Cache(键值缓存)。生成式文本预测每次生成一个 Token,都需要保留历史 Token 的 Key 和 Value。如果不做优化,只能为每个请求预分配一个固定最大长度(如 4096)的连续显存块,这会导致大量的显存碎片和显存浪费。

  • PagedAttention 内存分页机制:vLLM 借鉴了操作系统的虚拟内存分页思想,将 KV Cache 切成离散的 Block(默认大小 16),随用随分。这让显存浪费率从接近 70% 骤降到 4% 以下。
  • Continuous Batching(连续批处理):传统批处理必须等 Batch 内所有请求都生成完毕才能进行下一轮;vLLM 在 Iteration 级别进行调度,某个请求先生成结束,立即腾出空间接入新请求,吞吐量直接成倍拉升。

二、生产级 Docker 编排与 vLLM 核心参数避坑

为了兼顾环境隔离与可移植性,直接采用 Docker 部署。但这里有几个极容易踩雷的显存与通信参数,稍微配错就会导致启动崩溃或吞吐腰斩。

# docker-compose.yml
version: '3.8'

services:
  vllm-engine:
    image: vllm/vllm-openai:v0.6.6.post1
    container_name: vllm-engine
    runtime: nvidia
    restart: always
    environment:
      - CUDA_VISIBLE_DEVICES=0,1
      - NCCL_P2P_DISABLE=1 # 双 4090 没有 NVLink,必须禁用 P2P 防止 PCIe 通信死锁
    volumes:
      - /data/huggingface/hub:/root/.cache/huggingface
      - /data/models/Qwen2.5-32B-Instruct-AWQ:/model
    ports:
      - "8000:8000"
    ipc: host # 必须挂载宿主机 IPC,否则多卡张量并行(TP)内存交换会报 Bus error
    command: >
      --model /model
      --served-model-name qwen-32b
      --tensor-parallel-size 2
      --max-model-len 8192
      --gpu-memory-utilization 0.92
      --swap-space 16
      --max-num-seqs 64
      --enforce-eager
      --trust-remote-code

核心参数排障要点:

  1. --gpu-memory-utilization 0.92:默认是 0.9。在 24G 显存卡上,剩余的 8% 必须留给 PyTorch 的运行时上下文和 CUDA 图捕获。如果拉得太满(比如 0.98),模型初始化一完成,稍微来个长提示词请求立刻 CUDA OOM。
  2. --tensor-parallel-size 2:双卡并行推理。注意消费级主板(如两张 4090 走 PCIe 通道)没有 NVLink,跨卡同步开销大,必须加上 NCCL_P2P_DISABLE=1,否则在载入权重时可能直接卡死。
  3. --max-num-seqs 64:控制并发处理的最大序列数。不要盲目相信无上限并发,设为 64 或 128 是单机性能的最稳平衡点,超过这个数量排队的请求进入等待队列,反而能维持每个连接的吐字速度。
  4. --swap-space 16:分配 16GB 内存作为 CPU 交换空间。当并发请求突增、显存中的 KV Cache 实在放不下时,vLLM 会把部分 Block 移到内存,避免请求直接报错断开。

三、网关层调优:Caddy 代替 Nginx 实现丝滑 SSE

很多同学直接用默认配置的 Nginx 做反向代理,结果前端收到的大模型流式输出不是逐字吐出,而是一坨一坨卡顿输出,甚至直接超时报错。这是因为传统反向代理开启了 响应缓冲(Proxy Buffering),并且超时阈值太短。

相比 Nginx 复杂的配置语法,我推荐使用轻量、原生支持 HTTP/2 & HTTP/3 并且自动配置 Let’s Encrypt 证书的 Caddy。以下是经过实战验证的 Caddy 配置:

# Caddyfile
api.nassky.top {
    # 启用 gzip 和 zstd,但必须排除大模型流式输出的文本类型,防止缓冲区积压
    encode {
        zstd
        gzip
        match {not header Content-Type text/event-stream*}
    }

    # 路由反向代理至 vLLM 实例
    handle /v1/* {
        reverse_proxy vllm-engine:8000 {
            # 关键:彻底关闭缓冲,强制每个 token chunk 实时透传
            flush_interval -1

            # 长文本生成可能耗时极长,调高后端读写与响应头超时时间
            transport http {
                response_header_timeout 600s
                read_timeout 600s
                write_timeout 600s
                keepalive 120s
            }
        }
    }

    # 健康检查与常规路由
    handle {respond "LLM Gateway Ready" 200}
}

关键配置项拆解:

  • flush_interval -1:核心中的核心。告诉 Caddy 收到后端 vLLM 生成的一个 token 就立刻 flush 给客户端,彻底消除网络代理层的等待抖动。
  • 压缩过滤 (not header Content-Type text/event-stream*):压缩工具往往需要攒够一定字节的 Block 才会进行数据压缩,如果不做排除,流式传输依旧会被阻塞。

四、真实压力测试数据对比

在双路 RTX 4090(总显存 48GB)环境,模型选用 Qwen2.5-32B-Instruct-AWQ 进行基准测试。测试工具选用 vLLM 官方自带的 benchmark_serving.py,输入提示词长度统一为 1024 tokens,输出长度设为 512 tokens:

并发量 (Concurrency) 首字延迟 (TTFT Mean) 平均每秒吐字 (Token/s) 请求成功率
10 0.28 秒 118.5 100%
32 0.65 秒 312.4 100%
64 1.42 秒 480.2 100%
100 (超饱和) 3.89 秒 492.1 99.2% (少数排队超时)

实测数据表明,在 64 并发以内,服务整体吞吐量(Tokens/s)呈现出极其优秀的线性增长,平均首字返回时间控制在 1.5 秒以内。由于 Continuous Batching 的存在,整个推理卡的核心利用率全程稳定在 95% 以上,没有出现显存碎片断崖式下跌的问题。

五、极客运维心得与踩坑总结

搞私有化大模型落地,拼的不是谁卡多,而是资源利用率压榨和稳定性设计。以下是三条血泪经验:

  1. 警惕长上下文的显存陷阱:现在的模型动辄宣称支持 128k 上下文,但别在生产参数里无脑把 --max-model-len 开到顶。一个 128k 的请求进场,直接霸占十几 G 的 KV Cache,后续所有并发请求瞬间被卡死。一般业务建议强制限制在 8192 或 16384,超长文本走专用批处理通道。
  2. 善用 Chunked Prefill:如果你的场景包含大量的多轮对话(即输入长、输出短),务必在启动命令加入 --enable-chunked-prefill。它会把大段的 Prompt 预热打散到多个 Iteration 中,大幅降低突发长输入对其他正在吐字请求的瞬间延迟冲击。
  3. 监控先行:vLLM 原生暴露了 Prometheus 监控指标(默认路径 /metrics)。重点盯住 vllm:num_requests_waiting(等待请求数)和 vllm:gpu_cache_usage_factor(KV Cache 占用率),一旦 Cache 超过 0.95 且 Waiting 开始激增,就该做熔断或集群水平扩容了。
正文完
 0
评论(没有评论)