共计 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 劣化极其夸张,因为后续请求全部卡死在全量