告别 Ollama 并发瓶颈:单卡 4090 基于 SGLang 极致榨干 DeepSeek-R1 吞吐踩坑实录

2次阅读
没有评论

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

上个月我给小团队折腾一套基于本地大模型的内部 Code Agent,机器是一台配备单张 RTX 4090 24G 显卡、AMD 7950X 与 64GB 内存的专用 Linux 塔式服务器。起初为了省事直接拉了 Ollama 跑 DeepSeek-R1-Distill-Qwen-14B,单人测试时响应极快,体验极佳。

但当 3 个工程师同时挂载 IDE 插件、让 Agent 在数百行代码上下文中频繁轮询补全和思考时,问题暴露无遗:首字延迟(TTFT)从几百毫秒飙升到 6 秒以上,显存调度频繁引发抖动,甚至偶尔直接 OOM 崩溃。经过一周的对比重构,我彻底剥离了 Ollama,全面切到以 RadixAttention 著称的 SGLang。实测在 8 并发长上下文场景下,整体吞吐(TPS)暴涨了近 3.8 倍。

一、为什么搞团队级 Agent 必须放弃 Ollama?

Ollama 是个人本地轻度把玩的神器,封装极佳,但它的底层调度设计并不是为了多请求高吞吐而生的:

  • KV Cache 复用机制较弱: 在多轮对话或多 Agent 交互中,系统提示词(System Prompt)和上文大部分是完全静态的。Ollama 的前缀缓存机制面对交替交织的并发上下文时,缓存命中率惨不忍睹,大量算力浪费在重复计算 Prefill 上。
  • Continuous Batching(连续批处理)调优粒度粗: 虽然 Ollama 背后是 llama.cpp,但暴露出来的底层动态批处理参数极其有限,在显存临界点容易发生被动排队。
  • 显存利用率保守:Ollama 很难精细化压榨静态显存与动态 KV Cache 池的比例,显存剩余 4G 却不敢开更大的批次大小。

相比之下,SGLang 与 vLLM 才是生产级推理引擎的正解。SGLang 核心的 RadixAttention 把 KV Cache 维护成一棵基数树(Radix Tree),极其适合多 Agent 分叉调用和多轮聊天——前面共用的上下文只算一次,直接共享指针。

二、环境准备与量化模型选型实测

单张 4090 显存上限是 24GB,跑完整 FP16 的 14B 模型显存几乎直接打满,根本没有空间留给 KV Cache;跑 7B 模型性能有余但推理思考深度明显不够看。因此,黄金平衡点是 AWQ 或 GGUF/GPTQ 4-bit 量化的 14B / 32B 模型 。

为了榨干算力,我选择 deepseek-ai/DeepSeek-R1-Distill-Qwen-14B 的 AWQ 量化版本。相比 GGUF,AWQ 在 Tensor Core 上的 GEMM 计算效率在现代英伟达架构上更加暴力。

1. Docker Compose 基础设施搭建

不要在宿主机折腾复杂的 Python 虚拟环境与 CUDA 驱动绑定,SGLang 官方提供的 Docker 镜像是最省心的做法。编写 docker-compose.yml:

version: '3.8'

services:
  sglang-server:
    image: lmsysorg/sglang:latest
    container_name: sglang-deepseek
    runtime: nvidia
    restart: unless-stopped
    environment:
      - CUDA_VISIBLE_DEVICES=0
      - HF_HUB_ENABLE_HF_TRANSFER=1
    volumes:
      - /data/huggingface/hub:/root/.cache/huggingface/hub
    ports:
      - "30000:30000"
    ipc: host
    deploy:
      resources:
        reservations:
          devices:
            - driver: nvidia
              count: 1
              capabilities: [gpu]
    command: >
      python3 -m sglang.launch_server
      --model-path casperhansen/deepseek-r1-distill-qwen-14b-awq
      --host 0.0.0.0
      --port 30000
      --quantization awq
      --mem-fraction-static 0.82
      --context-length 16384
      --schedule-conservativeness 0.3
      --chunked-prefill-size 2048
      --trust-remote-code

2. 核心启动参数避坑解析

  • --mem-fraction-static 0.82:这是单卡运行的关键参数。表示模型权重和初始空间占用显存的 82%(约 19.6GB),剩下的显存硬切给运行时 KV Cache。如果设到 0.9,长上下文多并发瞬间 CUDA OOM;设太小则浪费吞吐能力。
  • --context-length 16384:DeepSeek-R1 思考链动辄上万字,但没必要无脑开到 32k,16k 是 24G 显存下的甜点位。
  • --chunked-prefill-size 2048:开启分块 Prefill。如果没有这行参数,当某个请求突然送进来 6000 字符的大代码块时,计算 Prefill 会让 GPU 计算单元瞬间占满,导致正在流式输出(Decode 阶段)的其他请求出现肉眼可见的卡顿。

三、生产级反向代理避坑:Nginx 与 SSE 机制

SGLang 启动后会暴露 OpenAI 兼容的 /v1/chat/completions 接口。但我发现,前端在接入打字机流式效果时,往往要等待 2~3 秒甚至一次性吐出一大串文字,完全丧失了流式的丝滑感。排查后发现是反代链路的缓冲(Buffer)在捣鬼。

如果你在 SGLang 前端挂了 Nginx 作为统一网关或内网穿透代理, 必须强制关闭代理缓冲并配置 chunked 传输 :

server {
    listen 80;
    server_name llm.internal.nassky.top;

    location /v1/ {
        proxy_pass http://127.0.0.1:30000/v1/;
        
        # 核心防卡顿配置:关闭缓冲并维持长连接
        proxy_http_version 1.1;
        proxy_set_header Connection "";
        proxy_buffering off;
        proxy_cache off;
        chunked_transfer_encoding on;

        # 思考模型思考耗时极长,调大超时时间防止客户端掉线
        proxy_read_timeout 600s;
        proxy_send_timeout 600s;

        proxy_set_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

另外,在 Python 后端或者客户端调用时,务必处理客户端主动断连的异常。当用户点击了 UI 上的“Stop Generation”,如果反代没有正确向下游传递 TCP RST 信号,GPU 还会傻傻地继续把剩余的 Reasoning Token 推理完,极度浪费显存和算力。

四、实测性能对决:Ollama vs SGLang

为了客观检验效果,我编写了一个异步测试脚本,模拟真实工作环境:构造 5 个具有重叠 System Prompt 和长上下文的编程任务,施加 1 到 8 个并发 worker,测试 DeepSeek-R1-Distill-Qwen-14B 在不同负载下的指标表现。

框架引擎 并发数 首字延迟 (TTFT) 整体吞吐量 (TPS) 显存占用 Radix Cache 命中率
Ollama (v0.5.x) 1 480ms 42.5 tokens/s 21.8 GB 低 / 不稳定
Ollama (v0.5.x) 4 2,450ms 58.2 tokens/s 23.2 GB 低
Ollama (v0.5.x) 8 6,890ms 61.0 tokens/s(伴随丢包) 23.6 GB 趋近于 0
SGLang (v0.4.x) 1 430ms 46.1 tokens/s 22.5 GB(固定池) 86%
SGLang (v0.4.x) 4 820ms 168.4 tokens/s 22.5 GB(固定池) 88%
SGLang (v0.4.x) 8 1,420ms 231.8 tokens/s 22.5 GB(固定池) 92%

从实测数据可以清晰看到两者的差距:在单并发时,由于都是在跑 AWQ 内核,两者速度相差不大;但一旦拉升到 8 并发,Ollama 的吞吐量几乎在 60 TPS 封顶,TTFT 劣化极其夸张,因为后续请求全部卡死在全量

正文完
 0
评论(没有评论)