单卡跑满并发:从 vLLM 到 SGLang 的高吞吐推理调优与流式代理踩坑记

7次阅读
没有评论

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

上个月在为团队内网搭建一套多 Agent 协作工作流时,我遇到了一个极其恶心的性能瓶颈:业务端接入了沉重的 System Prompt 和复杂的 ReAct Tool Schema,光是固定前缀就吃掉了 2.5k Token。当并发请求数稍微冲上 15 左右,原先跑得挺稳的 vLLM 实例的首字延迟(TTFT,Time to First Token)瞬间从 300ms 飙到了 3 秒以上,GPU 显存明明还有空余,推理吞吐却直接卡死在 Prefill 阶段。

为了把手里这块 RTX 4090(24G)的算力榨干,我花了一周时间把整套推理后端从 vLLM 迁移到了 SGLang,并顺带重构了前面的 Nginx 反代层。实测在多轮对话与高重合度 Prompt 场景下,吞吐量提升了近 2.8 倍。这篇笔记把整个迁移、调优以及中间踩过的“反直觉深坑”完整梳理出来,供各位自建 LLM 服务的同学参考。

一、痛点拆解:为什么 Agent 场景下 PagedAttention 开始吃力?

vLLM 普及了 PagedAttention,把 KV Cache 当作虚拟内存按 Page 分配,彻底解决了碎片化浪费,这一点毋庸置疑。但面对高频 Agent、多轮对话或 Few-shot 工作流时,核心瓶颈已经从“如何存 KV Cache”转移到了“如何复用 KV Cache”。

vLLM 虽然也提供了 Automatic Prefix Caching(APC),但它的实现本质上是基于 Hash 匹配的静态 Chunk。如果你的 Prompt 前缀发生哪怕一个空格的变化,或者在多分支树状采样(Tree Search / Multi-branch Agent)场景下,Hash 命中率就会断崖式下跌。而 SGLang 提出了 RadixAttention,它维护了一棵全局的基数树(Radix Tree):

  • 动态前缀树匹配: 不同请求只要有公共前缀(无论处于第几轮、长度是否整除 Block),都能在 Token 级别直接命中树节点,避免重复 Prefill。
  • LRU 树节点淘汰: 当 GPU 显存紧张时,SGLang 会优先驱逐叶子节点的 KV Cache,保留命中率最高的根节点(通常是 System Prompt 和 Schema)。
  • 原生 Runtime 协同: 调度器、显存管理和解释器直接打通,减少 Python 运行时的开销。

二、SGLang 生产级部署与参数压榨实战

我们选择的模型是经过 INT4-AWQ 量化的 Qwen2.5-14B-Instruct-AWQ,在 24G 显存的 RTX 4090 上,可以留出充裕的 KV Cache 空间支撑长上下文与并发。

1. 依赖与启动命令

建议直接使用官方 Docker 镜像,规避复杂的 FlashInfer 与 CUDA 依赖编译问题:

docker run --gpus all \
  --shm-size 32g \
  -p 30000:30000 \
  -v /data/models:/models \
  --ipc=host \
  --name sglang-server \
  lmsysorg/sglang:latest \
  python3 -m sglang.launch_server \
    --model-path /models/Qwen2.5-14B-Instruct-AWQ \
    --port 30000 \
    --host 0.0.0.0 \
    --mem-fraction-static 0.88 \
    --context-length 16384 \
    --chunked-prefill-size 2048 \
    --schedule-policy lpm \
    --enable-metrics

2. 核心参数逐项解析与踩坑点

  • --mem-fraction-static 0.88:静态显存分配比例。很多初学者喜欢拉到 0.95,结果导致 PyTorch 在运行时处理临时 Tensor 时直接 CUDA OOM 暴毙。对于 24G 显卡,0.85~0.88 是平衡 KV Cache 容量和显存突发开销的安全线。
  • --chunked-prefill-size 2048:非常关键的平滑参数。如果客户端发来一个 8k Token 的长文档,默认设置下引擎会全力进行 Prefill,导致正在进行的流式输出(Decode 阶段)全部停顿几十毫秒,终端用户感知就是“打字机突然卡顿”。分块 Prefill 可以在计算大块输入的同时穿插 Decode 计算。
  • --schedule-policy lpm:即 Longest Prefix Match。它强制调度器优先处理能最大程度复用当前 Radix Tree 节点的请求,大幅提升 Cache 命中率。
  • --shm-size 32g:切记不要用 Docker 默认的 64MB 共享内存,多进程通信(如使用 TP 张量并行或异步 IPC)会因为共享内存耗尽抛出 Bus error。

三、前端流式体验的“隐形杀手”:Nginx 反向代理调优

把 SGLang 跑起来后,很多开发者直接在前端配个 Nginx 转发就上线了,结果发现所谓的“打字机效果”根本不存在,前端要么死等几秒后一次性吐出所有文本,要么中途报 net::ERR_INCOMPLETE_CHUNKED_ENCODING。这是典型的 Nginx 缓冲与长连接配置失误。

以下是我线上经过高并发验证的 Nginx 配置段:

upstream sglang_backend {
    server 127.0.0.1:30000;
    keepalive 64; # 保持后端长连接池,避免频繁 TCP 握手
}

server {
    listen 80;
    server_name ai.nassky.top;

    location /v1/chat/completions {
        proxy_pass http://sglang_backend;

        # 核心:必须禁用缓冲以支持 SSE (Server-Sent Events)
        proxy_buffering off;
        proxy_cache off;

        # 解决 HTTP/1.0 导致的 Chunked 传输失效
        proxy_http_version 1.1;
        proxy_set_header Connection "";

        # 传递真实客户端信息
        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;

        # 大模型长思考与长生成时的超时控制
        proxy_connect_timeout 60s;
        proxy_send_timeout 300s;
        proxy_read_timeout 300s;

        # 禁用压缩,防止 gzip 缓存导致首字迟钝
        gzip off;
    }
}

踩坑要点:proxy_buffering off; 必须显式声明。否则 Nginx 会按照 proxy_buffer_size 凑满一块数据(通常是 4k 或 8k)才给客户端推一次,流式交互的毫秒级反馈直接沦为泡影。

四、实测压测数据对比:vLLM vs SGLang

为了客观评估,我们在相同硬件(Intel i9-13900K, 64GB DDR5, 单卡 RTX 4090 24G)下,使用包含 2,000 Token 固定 System Prompt、每次随机携带 500 Token 历史上下文的测试集,使用异步 Python 脚本发压 500 次请求,测试在并发度分别为 4、16、32 时的核心指标:

正文完
 0
评论(没有评论)
Copyright©2013-2025 Nas的天空 nassky.top 版权所有
 Theme by Puock
指标 / 引擎配置 并发量 (Concurrency) TTFT 首字延迟 (p95) 生成速度 (tokens/s) Cache 命中率
vLLM (启用 APC) 4 420 ms 68.2 78.4%
SGLang (RadixAttention) 4 145 ms 79.5 94.2%
vLLM (启用 APC) 16