同一块 32GB 显卡、同一个模型、同一套测量口径。结论不是「谁更好」,而是两条曲线在单流与并发上完全相反地分岔。
最后两格方向相反——这是本报告的核心结论,也是「两个引擎都留着」的理由。
一台消费级台式机,非数据中心硬件
测试期间 GPU 温度稳定在 32 °C,全程零降频(HW/SW Thermal Slowdown 与 Power Brake 均未触发),因此下列数字不含散热或功耗墙的影响。
全部取值抓自运行中的机器
b10502 · Windows CUDA 13.3 官方预编译Qwen3.8-27B-NVFP4-MTP-LOW.gguf · 15.53 GB--model Qwen3.8-27B-NVFP4-MTP-LOW.gguf
--ctx-size 262144 # 256K;实测与 32K 同速,几乎免费
--flash-attn on
--cache-type-k q8_0 # KV 量化,256K 上下文的前提
--cache-type-v q8_0
--n-gpu-layers 999
--spec-type draft-mtp # MTP 投机解码
--spec-draft-n-max 2 # 实测最优(模型卡推荐的 6 是最差档之一)
--spec-draft-p-min 0.75
vllm serve <model-dir> \
--max-model-len 32768 \
--gpu-memory-utilization 0.88 \
--max-num-seqs 32 # 受 Gated DeltaNet 的 Mamba cache 块数硬约束
--limit-mm-per-prompt '{"image":0,"video":0}'
先说怎么测的,数字才有意义
两个引擎用完全相同的基准脚本与判据,唯一差别是 endpoint。单流数字均为:
ignore_eos 固定 256 token、temperature=0、流式返回usage.completion_tokens,不数流式分片并发表格用另一口径:端到端聚合吞吐(含首 token 延迟与排队),两个引擎同样一致,因此可横比。
交互式使用的决定性指标
| 引擎 / 配置 | decode tok/s | 复现 | GPU 利用率 | 功耗 | 显存带宽 |
|---|---|---|---|---|---|
| llama.cpp · MTP n=2 | 离散 0.6% 5 实例 |
73% | 335 W | 45% | |
| llama.cpp · MTP n=4 | 5 实例 95.0–99.8 | 73% | 312 W | 37% | |
| llama.cpp · MTP n=5 | 单次 | 71% | 315 W | 40% | |
| llama.cpp · MTP n=3 | 单次 | 72% | 312 W | 40% | |
| llama.cpp · 不开 MTP | 单次 | 96% | 409 W | 64% | |
| llama.cpp · MTP n=6 (模型卡推荐值) | 单次 | 80% | 251 W | 28% | |
| vLLM · 无 MTP | 离散 0.3% 4 实例 |
98% | 335 W | 72% | |
| vLLM · MTP n=1 | 不可复现 9 实例,3 次低于基线 |
88–92% | 227–299 W | 38–56% |
换引擎贡献 1.39×(79.2 vs 57.0);MTP 再叠 1.33×(105.5 vs 79.2)。值得注意的是:同一个 MTP 功能,在 vLLM 里跨实例乱跳 46.7–73.4,在 llama.cpp 里离散仅 0.6%。选型时,可复现性比峰值更重要。
模型卡推荐 --spec-draft-n-max 6,在这块卡上是最差档之一(63.6,比完全不开 MTP 还慢 20%)。投机深度必须在自己的硬件上扫一遍。
结论与单流完全相反
| 并发 | llama.cpp 聚合 | vLLM 聚合 | vLLM 领先 |
|---|---|---|---|
| 1 | 0.89× | ||
| 2 | — | — | |
| 4 | 2.6× | ||
| 8 | 5.5× | ||
| 16 | 9.3× | ||
| 32 | — (已平台化在 ~72) | 14.7× |
llama.cpp 的聚合吞吐从并发 2 起就压在 ~73 tok/s 不再增长,vLLM 则近乎线性扩展到并发 32。这是设计取向的差异:vLLM 为批处理吞吐而生,llama.cpp 为单流延迟而生。
为公平起见把 llama.cpp 的并行槽位加到 --parallel 16 再测,结果反而崩塌:单流 6.5 tok/s、聚合 8.5 tok/s——比默认配置差 12 倍(日志显示 kv_unified=false,每槽位仅剩 16K 上下文)。不要试图把 llama.cpp 调成并发引擎。
一个不存在的优化目标
起初的目标是官方标称的 125.9 tok/s,据此以为 vLLM 的 57 有一倍差距可追。这个目标不成立——官方原文注明该数字测于 1× B200、128 并发。
| 平台 | 显存带宽 | 屋顶线上限 | 实测 | 硬件利用率 |
|---|---|---|---|---|
| 官方参考基准 · B200 @ batch 1 | ~8 TB/s | ~570 tok/s | 133.7 | 23% |
| 本次测试 · RTX 5090 @ 单流 | 1.79 TB/s | ~124 tok/s | 57.0 | 46% |
按每 token 读一遍约 14 GB 权重估算,这台机器对硬件的利用率是那份参考基准的两倍。GPU 实测 98% 利用率、335 W(上限 600 W)、32 °C、零降频——vLLM 侧没有 backend 可调,也没有空转可捡。后来的 1.85× 全部来自换引擎与 MTP,与「调后端」无关。
佐证:两个平台的聚合吞吐比值在并发 1 / 8 / 32 三档恒定在 38–42%。恒定比值 = 纯硬件差距;若是配置问题,比值会随 batch 变化。
延迟、思考模式、输出质量
| 引擎 | TTFT | 影响 |
|---|---|---|
| vLLM(32K 上下文) | 30 – 45 ms | 极短问答也无感 |
| llama.cpp(256K 上下文) | 348 – 553 ms | 约 10 倍。长输出无感,一问一答式短交互会觉出来 |
不是跑得更快,而是少生成 token。传 {"chat_template_kwargs":{"enable_thinking":false}}:
| 任务 | thinking 开 | thinking 关 | 提速 |
|---|---|---|---|
| 一句话解释 TCP 三次握手 | 1.73 s / 48 tok | 1.65 s / 42 tok | — |
| 翻译一句话 | 4.67 s / 133 tok | 0.30 s / 11 tok | 15.6× |
| 列 3 个内存泄漏原因 | 2.32 s / 75 tok | 1.52 s / 82 tok | 1.5× |
| 合计 | 8.7 s | 3.5 s | 2.5× |
翻译、格式化、抽取这类无需推理的任务收益最大;需要推理的题目照常开着。
| 探针 | 结果 |
|---|---|
| TCP 三次握手(中文一句话) | 正确、简洁 pass |
| 中译英 | 与 vLLM 输出逐字相同 pass |
| 9.11 与 9.9 谁大 | 答 9.9,正确 pass 经典陷阱题 |
该 GGUF 的 LOW 档指 LM head / embedding / MTP 头这 10 个「额外张量」的精度;448 个骨干张量在各档间字节相同。抽查未见质量退化。
本报告最值钱的部分
测试过程中有六次,探针给出了自洽但错误的答案。每一次若就此收工,得到的都是错结论。左栏是探针说的,右栏是实际。
流式分片计数 = 输出 token 数,得 47.3 tok/s
256 个 token 只收到 227 个非空分片,低估 18%。必须取服务端 usage.completion_tokens
开启 MTP 后 73.4 tok/s,+29% 免费提速
换 9 个实例后落在 46.7–73.4,其中 3 次低于基线。单点对比在漂移条件下无意义,必须交错 A/B
整台机器变慢了(同配置从 73 掉到 47)
同期另一臂的基线四次都是 57.0 纹丝不动。漂的是那条代码路径,不是机器——靠交错 A/B 才分辨出来
下载 5.66 MB/s,46 分钟能拉完 15.5 GB
那是 20 MB 突发测速。持续传输仅 0.06 MB/s(约 72 小时)。测可行性必须测持续速率
某投机参数启动失败,判为「不支持」
实例间只等了 4 秒,上一个的显存还没释放。给足释放时间后正常启动。切换必须轮询显存
文件写入成功——回显打印了 wrote (912 bytes)
路径被 shell 转义吃掉,写到了别处。回显是脚本自己 echo 的字符串,不是写盘证据
做任何「卡住 / 不通 / 不支持 / 已成功」的判断前,先问一句:我这个探针在什么情况下会给出假答案?假的肯定答案比假的否定答案更危险——它不会促使你继续查。
两条曲线分岔之后的实用判据
单人交互式使用。快 1.85×,256K 上下文几乎免费,冷启动仅 40 秒。代价是首 token 延迟约 10 倍,且并发不扩展。
批量任务、多路并发、需要低首 token 延迟。并发 8 时快 5.5×,并发 32 时 14.7×。代价是单流慢、冷启动 165 秒。
硬件相同或相近的话,把下面这段整体交给 AI coding agent(Claude Code / Codex / Cursor 等)即可
这套东西横跨 Windows、WSL2、计划任务、防火墙四个面,脚本很难覆盖各人机器的差异;而其中真正会卡住人的是六个反直觉的坑——把它们写进提示词,agent 才不会重走一遍弯路。下面这段已包含全部判据。
我有一台 Windows 11 工作站:AMD 9950X3D / 96GB 内存 / NVIDIA RTX 5090 32GB
(Blackwell sm_120),数据盘有 40GB 以上空闲。
请帮我部署 Qwen3.8-27B 本地推理服务。目标:单流解码 100+ tok/s、256K 上下文、
OpenAI 兼容 API。
以下方案已在同款硬件上实测验证,请照此执行,**不要自行改动标 🔴 的取值**。
### 1. 引擎选 llama.cpp,不要用 vLLM
- 同一张卡上 vLLM 单流仅 57 tok/s,llama.cpp 可达 105 tok/s
- 🔴 下载 llama.cpp 官方 **Windows CUDA 预编译包**(Linux 侧没有 CUDA 预编译包)
以及配套的 cudart 运行时包,解压到同一目录
- 只有在需要「多路并发批处理」时才该用 vLLM;单人交互式用 llama.cpp
### 2. 模型用带 MTP 头的 GGUF
- HuggingFace 仓库:esatapedico/Qwen3.8-27B-NVFP4-MTP-GGUF
- 文件:Qwen3.8-27B-NVFP4-MTP-LOW.gguf(约 15.5 GB)
- MTP 草稿头已烘在权重内,**不需要**另外准备 draft model
### 3. 启动参数
--model <模型路径>.gguf
--ctx-size 262144
--flash-attn on
--cache-type-k q8_0
--cache-type-v q8_0
--n-gpu-layers 999
--host 127.0.0.1
--port 8080
--spec-type draft-mtp
--spec-draft-n-max 2 # 🔴 必须是 2
--spec-draft-p-min 0.75
🔴 **`--spec-draft-n-max` 必须是 2。** 模型卡推荐的 6 在这块卡上只有 63 tok/s,
比完全不开 MTP(79)还慢。实测 2=105 / 3=93 / 4=96 / 5=95 / 6=64。
🔴 **不要加 `--parallel`。** 加到 16 会让单流从 105 崩到 6.5 tok/s(差 12 倍)。
llama.cpp 的并发吞吐从并发 2 起就不再增长,调它没有意义。
### 4. 进程托管:用「计划任务」,不要用 Start-Process
🔴 通过 SSH 或 Start-Process 拉起的 llama-server,**会随父会话退出而被杀**。
正确做法是注册一个计划任务,让它在**前台**运行 llama-server(任务持有该进程),
并把 ExecutionTimeLimit 设为 PT0S(无限),否则默认时限会把服务掐掉。
🔴 包装脚本里 **`$ErrorActionPreference` 不能设 `Stop`**。llama.cpp 把所有日志写
stderr,PowerShell 在 Stop 下会把它当成终止性错误,服务打印第一行就被自己
的包装脚本干掉。用 'Continue'。
- 建议把启动参数放进一个独立的 args.txt(一行一个),包装脚本读它再 splat 传入,
这样改配置不需要重新注册任务。
### 5. 如果要从别的机器访问
🔴 **Windows 会自动为 llama-server 创建 Block 规则**(首次监听时没人点「允许」的
默认后果,通常有 2 条)。而 **Windows 防火墙里 Block 优先于 Allow**——不先删掉
它们,你加多少放行规则都不会生效,且现象是「端口不通」,极易误判成配置写错。
- llama-server 用 `--api-key-file <文件>` 加鉴权(比 `--api-key` 好:密钥不进
命令行、不出现在进程列表)
- 防火墙放行规则要**按来源网段收窄**,不要对所有来源开放
- ⚠️ 注意 `/v1/models` 与 `/health` 不受密钥保护(llama.cpp 的设计),只有推理
端点受保护
### 6. 验证时请遵守这些判据,否则会得到假结论
🔴 **测 tok/s 不要数流式分片**,要取服务端返回的 usage.completion_tokens。
数分片会低估约 18%(256 个 token 只对应约 227 个非空分片)。
🔴 **不要用单次测量做横向对比。** 换配置要交错 A/B/A/B,否则任何时间漂移都会
被误读成配置差异。同一配置至少跨 3 个独立实例复现再下结论。
🔴 **重启换配置时,要轮询等显存真正降下来再启动下一个实例**(等到 4 GB 以下)。
只等固定几秒会因显存未释放而启动失败,而失败信息看起来很像「该参数不被支持」。
🔴 **不要把命令的回显当作执行成功的证据。** 要用独立命令复核实际状态
(文件是否存在、进程是否在跑、端口是否在听)。
### 7. 完成后请告诉我
- 实测的单流 decode tok/s(按上面第 6 条的口径)
- 显存占用
- 服务重启后是否仍在运行(验证托管是否真的生效)
| 指标 | 参考值 |
|---|---|
| 单流 decode | ≈105 tok/s |
| 显存占用 | ≈30.2 / 32.6 GB |
| 上下文 | 262,144 token |
| 冷启动 | ≈40 秒 |
| 首 token 延迟 | 350–550 ms |
若你的显卡不是 32GB,把 --ctx-size 调小即可——实测 32K 与 256K 速度基本相同,上下文长度几乎不影响解码速率,只影响显存占用。