# 5.7 在你自己的机器上测速：单流、并发、两种量化

这份代码回答三个问题。一个小模型在你的机器上一路能跑多快；同时来好几路请求时，每一路慢多少、总共能出多少 token；同一个模型的两种量化（4-bit 和 8-bit）速度差多少。宣传里的 tok/s 往往只报其中一个数，这份代码把它们放在一张表里。

做法：起一个本地推理服务，同时发 1、4、8、16 路请求（显卡上再加 32 路）。每个请求约 800 token 输入，让模型从 1 往上数数，用 `max_tokens` 卡在 256 个输出 token，保证每一路的活一样多。记三个数：首 token 延迟、每一路的生成速度、所有路加起来每秒出多少 token。

有两个脚本：

- `bench.py`：对任何兼容 OpenAI 接口的本地服务测速，llama.cpp 的 `llama-server`、vLLM 都可以。
- `bench_mlx.py`：Mac 上的 MLX，直接在 Python 进程里调 mlx-lm 测，输出同样的表。

模型都是 Qwen3.5-2B（后训练版，关闭思考模式）。

## 需要什么设备

- Apple Silicon Mac：llama.cpp 和 MLX 都能跑，8 GB 内存够用。
- 一张 NVIDIA 显卡：llama.cpp（CUDA 版）和 vLLM 都能跑；vLLM 需要 Linux（或 WSL2），8 GB 显存够用。

## 安装与运行

```bash
cd 5_7_local_speed
uv venv && source .venv/bin/activate       # 没有 uv 就用 python3 -m venv .venv
uv pip install -r requirements.txt         # 只有 httpx
```

**llama.cpp（Mac 或 NVIDIA）。** Mac 上 `brew install llama.cpp`；NVIDIA 显卡按 llama.cpp 仓库的说明编译 CUDA 版。`-np` 是服务能同时处理几路，要不小于你测的最大并发：

```bash
llama-server -hf unsloth/Qwen3.5-2B-GGUF:Q4_K_M --no-mmproj -ngl 99 -c 65536 -np 16 --port 8080
python bench.py --url http://127.0.0.1:8080 --label "llama.cpp Q4_K_M"
# 停掉服务，换成 Q8_0 再测一遍
llama-server -hf unsloth/Qwen3.5-2B-GGUF:Q8_0 --no-mmproj -ngl 99 -c 65536 -np 16 --port 8080
python bench.py --url http://127.0.0.1:8080 --label "llama.cpp Q8_0"
```

**MLX（Mac）。**

```bash
uv pip install mlx-lm
python bench_mlx.py --model mlx-community/Qwen3.5-2B-4bit --label "MLX 4bit"
python bench_mlx.py --model mlx-community/Qwen3.5-2B-8bit --label "MLX 8bit"
```

**vLLM（NVIDIA，Linux）。** 两种精度：原始的 bf16，和 vLLM 在加载时把权重转成的 FP8：

```bash
uv pip install vllm
vllm serve Qwen/Qwen3.5-2B --port 8000 --max-model-len 4096 --language-model-only
python bench.py --url http://127.0.0.1:8000 --label "vLLM bf16" --concurrency 1 4 8 16 32
vllm serve Qwen/Qwen3.5-2B --port 8000 --max-model-len 4096 --language-model-only --quantization fp8
python bench.py --url http://127.0.0.1:8000 --label "vLLM fp8" --concurrency 1 4 8 16 32
```

每次结果打印成一张表，同时存进 `results/`。

## 该看到什么

我们的样例输出在 `sample_output/`（2026-10-23）。Mac Studio（M3 Ultra）上的结果，每格是“每一路的生成速度 / 所有路加起来”，单位 token/秒：

| 并发路数 | llama.cpp Q4_K_M | llama.cpp Q8_0 | MLX 4-bit | MLX 8-bit |
|---|---|---|---|---|
| 1 | 183 / 163 | 160 / 146 | 272 / 244 | 193 / 178 |
| 4 | 82 / 269 | 94 / 301 | 197 / 606 | 168 / 537 |
| 8 | 48 / 307 | 56 / 344 | 149 / 830 | 127 / 738 |
| 16 | 30 / 360 | 31 / 380 | 87 / 923 | 83 / 896 |

一张 RTX 5090（32 GB）上的结果，同样是“每一路 / 所有路加起来”：

| 并发路数 | vLLM bf16 | vLLM FP8 | llama.cpp Q4_K_M | llama.cpp Q8_0 |
|---|---|---|---|---|
| 1 | 323 / 309 | 390 / 368 | 490 / 401 | 414 / 348 |
| 4 | 264 / 951 | 372 / 1280 | 290 / 851 | 264 / 804 |
| 8 | 256 / 1771 | 369 / 2416 | 160 / 881 | 159 / 873 |
| 16 | 229 / 3112 | 319 / 4081 | 76 / 855 | 74 / 845 |
| 32 | 226 / 5647 | 254 / 6073 | 39 / 879 | 39 / 895 |

几个现象：

- **单流速度和总吞吐是两个数。** 在 Mac 上，并发从 1 路加到 16 路，每一路都变慢了（MLX 4-bit 从 272 降到 87），但总共出的 token 涨到将近 4 倍（244 到 923）；在显卡上用 vLLM，从 1 路到 32 路总吞吐涨到 18 倍（309 到 5,647）。只报单流速度会低估一台机器能服务多少人，只报总吞吐会让人以为每个用户都能拿到那么快的速度。
- **首 token 延迟随并发上涨。** 同时进来的请求要排队做 prefill，16 路时 llama.cpp 的首 token 延迟从 0.17 秒涨到 2.7 秒。
- **4-bit 不一定比 8-bit 快。** 单流时 4-bit 快（权重少、读内存少）；在 llama.cpp 上，并发到 4 路以后 Q8_0 反而比 Q4_K_M 快。常见的解释是：并发高了以后瓶颈从读权重转向计算，而 4-bit 权重每次用之前都要先解压，多出来的计算抵掉了少读内存的好处。我们没有单独验证这个解释。
- **推理栈决定了并发时的表现。** 在显卡上，单流最快的是 llama.cpp Q4_K_M（每秒 490 token），比 vLLM 还快；但 llama.cpp 的总吞吐到 8 路就停在每秒 880 左右，再加并发只是把每一路摊薄，32 路时每路只剩 39，首 token 延迟涨到 2.6 秒。vLLM 每一路掉得很慢，32 路时每路还有 226–254，总吞吐到每秒 5,600–6,100，首 token 延迟 0.3 秒。一个人用和给一群人提供服务，该选的推理栈不一样。
- **显卡上的 FP8 在每个并发档都比 bf16 快**（单流 390 对 323，32 路 6,073 对 5,647），因为要读的权重少了一半，5090 也有原生的 FP8 计算单元。
- **同一个模型，不同推理栈差很多。** 同样在 Mac 上、同样 4-bit 左右，MLX 的总吞吐是 llama.cpp 的 2.5 倍左右。我们也试过 mlx-lm 自带的 HTTP 服务（`mlx_lm.server`，0.32.2 版），单流只有 41–74 token/秒，并发 8 路时直接拒绝连接；所以 MLX 用 `bench_mlx.py` 在进程里测，这测的是引擎本身。实际部署时，服务层同样会吃掉速度。

这些数字只说明“能跑多快”，不说明量化后质量掉了多少。量化对质量的影响，看公开的评测（正文 5.7 有引用），或者在你自己的任务上跑一遍对比。

## 换成你自己的数据

换模型：llama.cpp 换 `-hf` 后面的 GGUF 仓库和量化名，MLX 换 `--model`，vLLM 换 `serve` 后面的模型名。换负载：`--max-tokens` 改输出长度；想测长输入，改 `bench.py` 里的 `FILLER`（现在约 800 token）。`--concurrency` 改成你预计的同时在线人数。最该测的是你实际会用的组合：你会部署的那个推理栈、你会用的量化、你预计的并发。
