在一张 4090 上跑通 Qwen3:vLLM、SGLang、Ollama 三种本地部署方案的工程化对比

0 阅读11分钟

图片.png 本文面向:手上有 24GB 级别消费/工作站显卡,准备在本地或小规模生产环境跑大模型的工程师。

涉及框架版本:vLLM 0.6.x、SGLang 0.3.x、Ollama 0.5.x(2026 年 5 月)。

0. 写在前面

"本地部署大模型"这个话题,CSDN 上每个月都有大量新文章。但绝大多数都停留在两类内容:

"安装教程"型:照着官方 README 抄一遍,跑通 demo 就结束。

"性能党"型:上来甩一张图说 A 比 B 快多少倍,没有方法论、没有可复现脚本,读完不知道该选谁。 这篇我想做的是中间那一层——把三个目前最被讨论的框架(vLLM / SGLang / Ollama)放在同一台机器、同一个模型、同一组请求下,给你一套完整的对比方法、部署命令和决策依据

读完之后你应该能回答两个问题:

  1. 我自己的业务场景,应该选哪一个?

  2. 我手上这张卡,能跑多快、能撑多大并发?

1. 测试环境

为了让结果有可比性,本文以下面这套配置为基准。你换成自家硬件,按照同样方法跑一遍就能得到自家数据。

| 项目 | 配置 |

|---|---|

| GPU | NVIDIA RTX 4090(24GB)|

| CPU | Intel i9-14900K |

| 内存 | 64GB DDR5 |

| 操作系统 | Ubuntu 22.04 LTS |

| CUDA | 12.4 |

| 驱动 | 550+ |

| Python | 3.11 |

| 模型 | Qwen3-8B(HF 原版,BF16)|

| 测试集 | ShareGPT V3 抽样 200 条 |

为什么选 Qwen3-8B(BF16):

BF16 单卡刚好能放下(约 16GB 权重 + KV cache),不用量化,避免引入"框架对量化支持差异"这种额外变量;

8B 体量是真实业务里跑得起的"性价比甜点",比 7B 略大但能力更强;

Qwen3 是 2026 年五月最被广泛使用的开源中文模型,三个框架都原生支持。

如果你想跑 14B / 32B,思路完全一样,只是 32B 这一档需要 INT4/INT8 量化才能塞进 24GB。

2. 公平测试的几条铁律(别犯)*

我看过太多"vLLM 比 Ollama 快 10 倍"的文章,仔细一看发现条件根本对不上。这里先把规则定清楚:

  1. 同一模型权重——不要拿 vLLM 跑 BF16、Ollama 跑 Q4,那不公平。

  2. 同一 max_tokens——输出长度直接影响吞吐计算,固定为 256 或 512。

  3. 同一并发模式——单请求顺序 vs 多请求并发,分开测,分开报。

  4. 预热——前 5 个请求扔掉不计,避免冷启动污染。

  5. 报告完整指标——只看吞吐量不够,必须配 TTFT(首 token 延迟)  和 显存占用

下面三个框架的部署章节会给出完全一致的入参,便于横向对比。

3. 三个框架各自的"画像"

在动手之前,先用一段话把每个框架的设计取向讲清楚。这比任何 benchmark 数字都重要——选型从来不是"谁更快",而是"哪一个的取向匹配你的场景"

vLLM —— 生产级 Serving 的事实标准

核心创新是 PagedAttention,把 KV cache 当成分页内存管理,解决了显存碎片导致的并发瓶颈。

对高并发批处理的优化最深、最成熟,几百路并发下吞吐稳定

API 完全对齐 OpenAI 协议,前端代码几乎零成本切换。

缺点:单请求场景下延迟优势不明显;冷启动慢;对小模型/小流量场景"杀鸡用牛刀"。

SGLang —— 后起之秀,强在结构化输出和前缀复用

由 LMSys 团队主导(Vicuna、Chatbot Arena 也是他们做的)。

核心创新是 RadixAttention——一种比 PagedAttention 更激进的前缀缓存方式。

多轮对话、Agent 工具调用、Few-shot prompt 这种"共享前缀"场景,性能可以反超 vLLM。

结构化输出(JSON Schema / 正则约束)  是 SGLang 的招牌特性。

缺点:生态不如 vLLM 大,部分模型适配略晚;文档相对零散。

Ollama —— 上手最低、最适合"今天就要用上"

底层是 llama.cpp,主打 GGUF 量化模型 + 单进程跨平台运行。

装好之后一行命令拉模型、一行命令跑:ollama run qwen3:8b。

对 Mac、Windows、Linux 都友好,对没 N 卡的开发者特别友好。

缺点:高并发下吞吐被 vLLM/SGLang 拉开较大差距;调度策略简单;对生产级监控/限流的支持要靠外挂。

一句话总结:

想拼并发吞吐选 vLLM;想拼 Agent/对话场景选 SGLang;想拼上手速度选 Ollama。  

图片.png 三框架设计取向对比

4. 实操一:用 Ollama 跑起来(5 分钟)

最简单的先来。

安装(Linux)

curl -fsSL ollama.com/install.sh | sh 

拉模型并启动 daemon

ollama pull qwen3:8b

ollama serve &

测试

curl http://localhost:11434/v1/chat/completions \

  -H "Content-Type: application/json" \

  -d '{

    "model": "qwen3:8b",

    "messages": [{"role":"user","content":"用一句话解释 PagedAttention。"}],

    "stream": false

  }'

 

注意 localhost:11434/v1/... 这条路径——Ollama 从 0.4+ 开始已经对外暴露 OpenAI 兼容接口,这是它能进生产的关键一步。

调优建议:

默认只加载一个模型,需要并发可以设 OLLAMA_NUM_PARALLEL=4 和 OLLAMA_MAX_LOADED_MODELS=1;

显存吃满后 Ollama 会自动把部分层卸到 CPU,这时吞吐会断崖式下跌,要么换更小模型、要么用量化版(如 qwen3:8b-q4_K_M

5. 实操二:用 vLLM 跑起来

推荐用 conda/uv 隔离环境

pip install "vllm>=0.6.0"

启动 OpenAI 兼容服务

python -m vllm.entrypoints.openai.api_server \

  --model Qwen/Qwen3-8B \

  --dtype bfloat16 \

  --max-model-len 8192 \

  --gpu-memory-utilization 0.90 \

  --port 8000

启动以后同样通过 OpenAI 协议访问:

curl http://localhost:8000/v1/chat/completions \

  -H "Content-Type: application/json" \

  -d '{

    "model": "Qwen/Qwen3-8B",

    "messages": [{"role":"user","content":"用一句话解释 PagedAttention。"}]

  }'

几个关键参数解释:

--gpu-memory-utilization 0.90:告诉 vLLM 可以使用 90% 显存。这是吞吐的关键开关,太低会浪费,太高会 OOM。

--max-model-len 8192:上下文长度。开太长会吃掉 KV cache 预算,并发数随之下降。

--enable-prefix-caching:开启前缀缓存,对系统 prompt 固定的场景有 20%+ 提升,强烈建议加上。

6. 实操三:用 SGLang 跑起来

pip install "sglang[all]>=0.3.0"

启动 OpenAI 兼容服务

python -m sglang.launch_server \

  --model-path Qwen/Qwen3-8B \

  --dtype bfloat16 \

  --context-length 8192 \

  --mem-fraction-static 0.85 \

  --port 30000

SGLang 的杀手锏在结构化输出,举个例子:

curl http://localhost:30000/v1/chat/completions \

  -H "Content-Type: application/json" \

  -d '{

    "model": "Qwen/Qwen3-8B",

    "messages": [{"role":"user","content":"输出三个城市的天气信息,按下面 JSON 格式:[{name, temp, desc}]"}],

    "response_format": {"type": "json_object"}

  }'   SGLang 对 JSON Schema 的约束是编码层强约束,不是靠 prompt 软提示,所以几乎不会输出非法 JSON。这件事在做 Agent 工具调用时极其重要。

7. 性能压测:可以直接抄走的脚本

下面这个脚本三个框架都能用(因为接口都对齐了 OpenAI 协议),你只要换 BASE_URL 即可。

bench.py

import time, json, asyncio, statistics

import httpx

BASE_URL = "http://localhost:8000/v1"   # vLLM=8000 / SGLang=30000 / Ollama=11434

MODEL    = "Qwen/Qwen3-8B"              # Ollama 用 "qwen3:8b"

CONCURRENCY = 32

N_REQUESTS  = 200

MAX_TOKENS  = 256

PROMPT = "请写一段 200 字以内的科技公司业务介绍,要求包含三个产品名。"

async def one_request(client, idx):

    t0 = time.time()

    first_token_time = None

    n_tokens = 0

    async with client.stream(

        "POST", f"{BASE_URL}/chat/completions",

        json={

            "model": MODEL,

            "messages": [{"role": "user", "content": PROMPT}],

            "max_tokens": MAX_TOKENS,

            "stream": True,

        },

        timeout=120.0,

    ) as r:

        async for line in r.aiter_lines():

            if not line or not line.startswith("data:"):

                continue

            data = line[5:].strip()

            if data == "[DONE]":

                break

            try:

                chunk = json.loads(data)

                delta = chunk["choices"][0].get("delta", {})

                if delta.get("content"):

                    if first_token_time is None:

                        first_token_time = time.time()

                    n_tokens += 1

            except Exception:

                pass

    t1 = time.time()

    return {

        "ttft": (first_token_time - t0) if first_token_time else None,

        "latency": t1 - t0,

        "tokens": n_tokens,

    }

async def main():

    sem = asyncio.Semaphore(CONCURRENCY)

    async with httpx.AsyncClient() as client:

        async def bounded(i):

            async with sem:

                return await one_request(client, i)

        t_start = time.time()

        results = await asyncio.gather(*[bounded(i) for i in range(N_REQUESTS)])

        t_end = time.time()

    # 丢弃前 5 个预热请求

    results = results[5:]

    ttfts   = [r["ttft"] for r in results if r["ttft"]]

    lats    = [r["latency"] for r in results]

    total_tokens = sum(r["tokens"] for r in results)

    elapsed = t_end - t_start 

    print(f"并发: {CONCURRENCY}  总请求: {N_REQUESTS}")

    print(f"耗时: {elapsed:.1f}s")

    print(f"吞吐: {total_tokens / elapsed:.1f} tokens/s")

    print(f"TTFT  p50/p95: {statistics.median(ttfts):.3f}s / {statistics.quantiles(ttfts, n=20)[-1]:.3f}s")

    print(f"端到端 p50/p95: {statistics.median(lats):.2f}s / {statistics.quantiles(lats, n=20)[-1]:.2f}s") 

if name == "main":

    asyncio.run(main())

跑法:

启动框架后

python bench.py

显存占用单独看:

nvidia-smi --query-gpu=memory.used,memory.free,utilization.gpu --format=csv -l 1

8. 你应当预期看到的结果范围

下面的结论来自社区公开 benchmark 与本人多次测试的归纳,不同硬件/驱动会有偏差,建议你跑完上面脚本以自己的数字为准。**

把同等条件下的三个框架放在一起,你大概率会看到下面这种格局:

| 维度 | Ollama | vLLM | SGLang |

|---|---|---|---|

| 单请求 TTFT | 较快 | 中等 | 较快 |

| 单请求吞吐 | 中等 | 较快 | 较快 |

| 并发吞吐(32 并发)  | 明显落后 | 第一梯队 | 第一梯队 |

| 长前缀复用场景 | 一般 | 良好 | 最强 |

| 结构化输出 | 弱 | 中等 | 最强 |

| 显存利用率 | 灵活但低 |  |  |

| 部署复杂度 | 最低 | 中等 | 中等 |

| 生产监控/限流生态 | 弱 | 最完善 | 中等 |

| 跨平台(Mac/Win/Linux)| 全部 | Linux 优先 | Linux 优先 |

几个值得注意的细节:

单请求顺序问答 场景下,Ollama 并不慢,体感差距很小。它真正被拉开的是并发。

prefix caching 开启后,三家差距会被显著拉近——这也是为什么千万别忘了打开。

SGLang 在 Agent 多轮 / Few-shot prompt 场景下,吞吐可以反超 vLLM 20%-40%,这是 RadixAttention 的功劳。

Ollama 的优势在 运维心智成本:写一行 docker run,10 分钟搭好服务这件事,vLLM/SGLang 都做不到。

9. 选型决策表(按业务场景)

不同场景给出推荐组合:

| 业务场景 | 推荐 | 备注 |

|---|---|---|

| 个人/小团队内部工具 | Ollama | 上手快、跨平台、维护成本最低 |

| 高并发 API 网关 | vLLM | 协议齐全、监控生态成熟 |

| Agent / 多工具调用 | SGLang | RadixAttention + JSON Schema 双 buff |

| RAG / 知识库问答 | vLLM | 稳定的高并发是核心 |

| 边缘设备 / 笔记本 | Ollama | 唯一能优雅跑在 Mac/Windows 的 |

| 需要严格 JSON 输出 | SGLang | 编码层强约束,几乎无非法输出 |

| 企业内多模型并行 | vLLM | 多模型路由生态完善 |

我个人在公司里的部署组合是:前端 nginx → vLLM(主流量)+ SGLang(Agent 业务专线)+ Ollama(开发/调试机) ,三家各管一摊,没必要二选一。

10. 踩坑清单

按出现频率排序,都是真实踩过的坑:

1)vLLM 启动报 OOM,但显存看着没满。

原因:--gpu-memory-utilization 默认是 0.9,但 vLLM 启动时会预分配 KV cache,需要"实测的连续空闲显存",被其他进程占了零碎的 1-2GB 就会失败。

解法:跑 nvidia-smi,先 kill 其他 Python 进程,或调低到 0.8。

2)SGLang 启动慢(超过 1 分钟)。

原因:第一次会编译 RadixAttention CUDA kernel。

解法:等。后续启动会用缓存。

3)Ollama 并发请求被串行化。

原因:环境变量 OLLAMA_NUM_PARALLEL 没设置,默认是 1。

解法:export OLLAMA_NUM_PARALLEL=4 后重启 ollama serve。

4)三个框架对同一 prompt 输出不一样。

原因:默认 temperature 和 top_p 不一致。

解法:压测时显式指定 temperature=0,让结果可复现。

5)TTFT 在 vLLM 上很高但吞吐很高。

原因:vLLM 把请求批起来一起跑,单请求会等一会,但整体吞吐上去了。

解法:这是设计取向,要 TTFT 优先选 SGLang 或开 --enable-chunked-prefill。

6)Qwen3 在三家上 tokenizer 报错。

原因:transformers 版本太低,没识别 Qwen3 词表。

解法:pip install -U transformers,至少 4.45+。

11. 结语

如果只能记住三句话,建议是这样:

  1. 别选"最快的",选"和你场景最匹配的"。  三家都在拼命迭代,半年后绝对数字会变,但设计取向不会变。

  2. 跑你自己的 benchmark。  别人家的数字仅供参考,硬件、模型、流量模式只要变一项,结论就可能翻车。把上面的 bench.py 改三个参数跑一遍,比读 100 篇文章都管用。

  3. 多框架共存是常态。  不要把"选型"理解成"非此即彼",生产环境里三家并存非常普遍,各管各的流量切片。

文中所有部署命令、压测脚本均原创整理,已在 RTX 4090 + Ubuntu 22.04 环境跑通。如果你按这套方法实测出与上表预期相差较大的数字,欢迎评论区留硬件配置和具体参数,我会更新到后续文章里。**

**

后续会写:《把 vLLM 推到 100 并发:参数调优全记录》《SGLang RadixAttention 在 Agent 场景下的性能拆解》。感兴趣可以先关注。