这篇能做出什么
按下面的流程走完,你会得到三样东西:
1. 一个在自有机器上编译通过、能加载 DeepSeek 系列模型权重并跑出通顺输出的推理进程;
2. 一套固定的压测脚本,给定并发数、输入长度、输出长度,能稳定复现 TTFT、单请求解码速度、整体输出 tokens/s;
3. 一张「同一台机器、同一份权重、同一组输入」下,DeepSeek 开源引擎与 vLLM 的吞吐与显存峰值对照表,用来判断在你的场景里该用哪个。
需要说明的是:DeepSeek 在开源周及后续陆续放出了推理相关的多个组件(算子库、通信库、专家负载均衡、推理框架代码等),各仓库的目录结构和启动方式会随版本变化。这篇给的是通用工程流程和可直接照抄的脚本,具体的仓库地址、模块名、API 名字以官方仓库 README 当前版本为准。
前置条件清单
- 硬件:至少一张数据中心级 GPU(A100/H100/H800 一类),显存能放下权重加 KV cache。想跑满血大模型的多卡机器,需要 NVLink 或至少高速 RDMA 互联。
- 驱动与 CUDA:
nvidia-smi能正常输出,驱动版本满足你要装的 PyTorch 与 CUDA Toolkit 要求;nvcc --version与 PyTorch 自带的 CUDA 版本必须一致。 - 系统:Linux(Ubuntu 22.04 一类),已装
build-essential、cmake、ninja-build、git、git-lfs。 - Python:3.10 或以上,建议用 venv 或 conda 做隔离。
- 磁盘:权重动辄上百 GB 到数百 GB,留出至少两倍权重大小的空间(下载 + 转换缓存)。
- 权限:能读写
/usr/local/cuda,或至少能把 CUDA 路径写进环境变量。 - 一张表格纸(或一个 csv),用来记录每次实验的配置,否则压测结果没有可比性。
---
步骤一:把硬件与软件基线钉死
先记录基线,后面所有数字都要和它对应。
```bash
nvidia-smi
nvidia-smi topo -m # 看多卡互联是 NVLink 还是 PCIe
nvcc --version
python -V
```
多卡机器务必看 topo -m:如果卡间是 SYS 或跨 NUMA 的 PCIe,张量并行的通信开销会明显吃掉吞吐,压测结果会和别人差一大截,这不是引擎的问题。
把 nvidia-smi 的输出截图存档。显存对比实验里,基线显存(空载占用)要减掉。
---
步骤二:建隔离环境并装 PyTorch
```bash
python -m venv .venv
source .venv/bin/activate
pip install -U pip wheel setuptools
```
PyTorch 的安装命令不要凭记忆写,用官网 Get Started 选择你的 CUDA 版本后生成的那一条,原样粘贴:
```bash
下面这行请替换为 PyTorch 官网当前生成的命令
pip install torch --index-url https://download.pytorch.org/whl/<cuda-tag>
```
装完立刻自检,两个版本号必须对得上:
```bash
python -c "import torch; print(torch.__version__, torch.version.cuda, torch.cuda.is_available())"
```
torch.version.cuda 和你 nvcc --version 里的大版本不一致时,先解决这个,再往下走。CUDA 扩展编译九成的玄学报错都出在这里。
再装常用依赖:
```bash
pip install transformers safetensors accelerate sentencepiece einops \
huggingface_hub aiohttp numpy tqdm
```
---
步骤三:拉代码并编译
```bash
git clone <官方仓库地址> # 地址见官方 README
cd <仓库目录>
cat README.md # 先读完再动手
```
仓库一般会有两条安装路径:纯 Python 包安装,或者带 CUDA 扩展的编译安装。推理引擎的核心算子(attention、MoE 路由、量化 GEMM)通常是 CUDA/C++ 写的,需要编译。
编译前设置两个关键环境变量:
```bash
export MAX_JOBS=$(nproc) # 并行编译任务数,内存小时调低,否则会 OOM
export TORCH_CUDA_ARCH_LIST="9.0" # Hopper 用 9.0,Ada 用 8.9,Ampere 用 8.0/8.6,按你的卡改
```
TORCH_CUDA_ARCH_LIST 写错会得到一个能编译、但运行时崩在 no kernel image is available for execution on the device 的二进制。查自己显卡的计算能力:
```bash
python -c "import torch; print(torch.cuda.get_device_capability())"
```
然后编译:
```bash
pip install -e . --no-build-isolation
```
部分仓库用 Makefile 或 setup.py build_ext,按 README 来:
```bash
make -j"$(nproc)"
或
python setup.py build_ext --inplace
```
编译通常要十几分钟到一小时。看到编译产物 .so 文件落地,再跑仓库自带的单测或示例脚本验证:
```bash
python -m pytest tests/ -x -q # 如果仓库带测试
```
编译缓存在 ~/.cache/torch_extensions,换 CUDA 版本或改架构参数后记得清掉,否则会复用旧的错误产物。
---
步骤四:下载并摆放权重
DeepSeek 系列权重是分片的 safetensors,同时带有 config.json、tokenizer 文件和生成配置。用官方工具下载到本地目录:
```bash
export HF_ENDPOINT=https://hf-mirror.com # 境外网络可去掉这行
hf download <模型仓库名> --local-dir ./weights --max-workers 8
```
下完核对文件数量与大小:
```bash
ls -lh ./weights | head -30
du -sh ./weights
python -c "import json;print(json.load(open('./weights/config.json')))"
```
config.json 里要关注几个字段:num_hidden_layers、num_attention_heads、num_key_value_heads、n_routed_experts、torch_dtype。后面调并行度和显存时都要用到。
关于 FP8 权重:DeepSeek 官方发布的权重是 FP8(block-wise 量化)格式。引擎是否支持直接加载 FP8 权重、是否需要先做离线反量化成 BF16,取决于你拿到的引擎版本。先按 README 说明来;如果不支持,反量化后的 BF16 权重体积大约是原来的两倍,显存规划要重新算。
分片数与并行度必须匹配:张量并行度 TP 一般要求能整除注意力头数;专家并行度 EP 要求能整除路由专家数。不整除时,加载阶段会报 shape mismatch 或直接静默算错。
---
步骤五:先跑通单条请求,再谈性能
不要一上来就压测。先用最短路径确认「能加载、能出话」。
写一个骨架脚本,把仓库 README 里给的初始化 API 填进去:
```python
sanity_check.py
import time, torch
from transformers import AutoAI 词典:Token">Tokenizer
WEIGHTS = "./weights"
tokenizer = AutoTokenizer.from_pretrained(WEIGHTS, trust_remote_code=True)
下面这行按官方 README 的引擎初始化写法替换
engine = YourEngine(model_path=WEIGHTS, tensor_parallel_size=1, ...)
engine = None
prompt = "用三句话解释什么是 KV cache。"
ids = tokenizer(prompt, return_tensors="pt").input_ids
t0 = time.time()
out = engine.generate(ids, max_new_tokens=128, temperature=0)
out = None
print(f"耗时 {time.time()-t0:.2f}s")
print(tokenizer.decode(out[0], skip_special_tokens=True))
```
这一步的验收标准只有三条:不报错、输出是通顺中文、同一 prompt 重复跑两次(贪心解码)结果一致。第三条能排除掉 kernel 写错、并行通信出错这类隐蔽问题。
加载阶段顺带记录两个数字:
```python
print("权重加载后显存:", torch.cuda.memory_allocated()/2**30, "GiB")
print("峰值显存:", torch.cuda.max_memory_allocated()/2**30, "GiB")
```
权重加载后的显存基本等于「模型大小 + 激活最小值」,这个数决定了你留给 KV cache 的空间。
---
步骤六:批处理与显存参数怎么调
推理引擎的吞吐几乎全由这几个参数决定,逐个说清楚它们影响什么:
| 参数(名称因引擎而异) | 作用 | 调大的后果 |
|---|---|---|
max_num_seqs / max_batch_size | 同时处理的请求数上限 | 吞吐上升,TTFT 上升,KV cache 吃紧 |
max_model_len / max_seq_len | 单请求最大上下文长度 | 直接线性放大 KV cache 预留量 |
gpu_memory_utilization | 允许引擎占用的显存比例 | 越大 KV cache 块越多,太大会 OOM |
block_size / page_size | KV cache 分页粒度 | 小块省显存但管理开销大,大块反之 |
tensor_parallel_size | 张量并行卡数 | 单卡显存下降,通信开销上升 |
expert_parallel_size | 专家并行卡数 | MoE 模型省显存的关键 |
调参顺序建议这样:先把 max_model_len 定成业务真实需要的最长值(不要图省事填模型上限),再把 gpu_memory_utilization 压到能稳定跑的水平,最后在这个显存预算内尽量抬高 max_num_seqs。
举个例子:一张 80GB 卡,BF16 权重占 60GB,那就只剩十几 GB 给 KV cache。如果 max_model_len 设成 128K,单条请求的 KV cache 就可能吃掉好几 GB,实际能并发的请求数只有个位数,吞吐自然上不去。把 max_model_len 降到 8K,同样显存能并发几十条,总吞吐可能翻几倍——这是压测里最容易拿到收益的一步。
---
步骤七:写压测脚本
为了能和 vLLM 公平对比,压测走 OpenAI 兼容的 HTTP 接口最省事——两边都用同一份脚本,只换端口。
```python
bench.py
import argparse, asyncio, json, random, time, statistics
import aiohttp
def make_prompt(n_tokens):
用重复的短句构造近似固定长度的输入,token 数用空白切分粗略控制
unit = "人工智能推理系统的吞吐测试需要稳定输入长度。"
return (unit * (n_tokens // 8 + 1))[: n_tokens * 2]
async def one(session, url, model, in_len, out_len, idx):
payload = {
"model": model,
"messages": [{"role": "user", "content": make_prompt(in_len)}],
"max_tokens": out_len,
"temperature": 0.0,
"stream": True,
}
t0 = time.perf_counter()
ttft, out_tokens = None, 0
async with session.post(url, json=payload) as resp:
async for raw in resp.content:
line = raw.decode("utf-8", "ignore").strip()
if not line.startswith("data:"):
continue
data = line[5:].strip()
if data == "[DONE]":
break
try:
obj = json.loads(data)
except json.JSONDecodeError:
continue
delta = obj["choices"][0].get("delta", {}).get("content")
if delta:
if ttft is None:
ttft = time.perf_counter() - t0
out_tokens += 1
total = time.perf_counter() - t0
return {"ttft": ttft or 0.0, "total": total,
"out_tokens": out_tokens, "idx": idx}
async def main():
ap = argparse.ArgumentParser()
ap.add_argument("--url", required=True)
ap.add_argument("--model", required=True)
ap.add_argument("--num", type=int, default=64)
ap.add_argument("--concurrency", type=int, default=8)
ap.add_argument("--in-len", type=int, default=512)
ap.add_argument("--out-len", type=int, default=256)
args = ap.parse_args()
sem = asyncio.Semaphore(args.concurrency)
async with aiohttp.ClientSession(
timeout=aiohttp.ClientTimeout(total=1800),
connector=aiohttp.TCPConnector(limit=args.concurrency + 8),
) as session:
async def guarded(i):
async with sem:
return await one(session, args.url, args.model,
args.in_len, args.out_len, i)
预热:前 4 条不计入统计
await asyncio.gather(*[guarded(i) for i in range(4)])
t0 = time.perf_counter()
results = await asyncio.gather(*[guarded(i) for i in range(args.num)])
wall = time.perf_counter() - t0
out_total = sum(r["out_tokens"] for r in results)
ttfts = [r["ttft"] for r in results if r["ttft"] > 0]
decode = [(r["total"] - r["ttft"]) / max(r["out_tokens"] - 1, 1)
for r in results if r["ttft"] > 0]
print(f"并发 {args.concurrency} 请求数 {args.num} "
f"输入/输出 {args.in_len}/{args.out_len}")
print(f"墙钟时间 {wall:.2f} s")
print(f"输出吞吐 {out_total / wall:.2f} tokens/s")
print(f"TTFT 均值/中位 {statistics.mean(ttfts):.3f} / "
f"{statistics.median(ttfts):.3f} s")
print(f"单请求 TPOT {statistics.mean(decode)*1000:.1f} ms/token")
if __name__ == "__main__":
asyncio.run(main())
```
用法:
```bash
python bench.py --url http://127.0.0.1:8000/v1/chat/completions \
--model deepseek --num 128 --concurrency 16 \
--in-len 512 --out-len 256
```
几个必须遵守的压测纪律:
- 并发要扫梯度,至少跑 1、8、16、32、64 这几档,画出「并发-吞吐」曲线。只看单点会得出错误结论。
- 固定输入输出长度。变长请求让结果不可比。
- 必须预热。首次请求会触发 JIT 编译、显存池扩张,混进统计里会让数据失真。
- 客户端不能成为瓶颈。并发拉到 64 以上时,用
nvidia-smi同时看 GPU 利用率;如果 GPU 利用率只有 40% 而客户端 CPU 打满,说明测的是客户端不是引擎。必要时多进程或多机发起压测。 - 说明统计口径。输出 tokens/s 和总 tokens/s(含输入)不是一回事,报数时写清楚。
---
步骤八:与 vLLM 对照
vLLM 也用 OpenAI 兼容接口,启动命令大致是这样(参数名以 vLLM 官方文档当前版本为准):
```bash
vllm serve ./weights \
--served-model-name deepseek \
--tensor-parallel-size 8 \
--max-model-len 8192 \
--gpu-memory-utilization 0.90 \
--port 8001
```
对照实验的唯一要求是控制变量:同一台机器、同一份权重、同一组 max_model_len 和显存比例、同一份压测脚本、同样的并发梯度。任何一项不同,比较就没有意义。
跑完把结果填进这张表(数字自己填,别抄别人的):
| 指标 | DeepSeek 开源引擎 | vLLM |
|---|---|---|
| 权重加载后显存 (GiB) | ||
| 峰值显存 (GiB) | ||
| 并发 1 输出吞吐 (tok/s) | ||
| 并发 16 输出吞吐 (tok/s) | ||
| 并发 64 输出吞吐 (tok/s) | ||
| 并发 64 TTFT 中位数 (ms) | ||
| 并发 64 TPOT (ms/token) | ||
| 加载耗时 (s) |
显存观测建议另开一个窗口采样,比单点读取更可靠:
```bash
nvidia-smi --query-gpu=timestamp,memory.used,utilization.gpu \
--format=csv -l 1 > vram.csv
```
经验规律(供你验证,不作为结论):DeepSeek 开源引擎在自家 MoE 架构上通常有更细的专家并行和负载均衡优化,长上下文与 MoE 场景可能占优;vLLM 的调度器、前缀缓存、生态兼容性更成熟,通用场景和小并发下往往更省心。具体到你的机器和模型,一定要自己跑。
---
常见坑与排错
1. undefined symbol 或 ImportError: libcudart.so...
PyTorch 的 CUDA 版本与系统 CUDA 不一致,或扩展编译时用的 CUDA 与运行时不同。清掉 ~/.cache/torch_extensions,统一版本后重编。
2. no kernel image is available for execution on the device
TORCH_CUDA_ARCH_LIST 与显卡算力不匹配。用 torch.cuda.get_device_capability() 查实际值,改完重新编译。
3. 编译卡死或被 OOM killer 杀掉
MAX_JOBS 设得比内存能承受的还高。降到 nproc/2 或 4 再试。
4. 加载权重报 shape mismatch
并行度与权重分片不匹配,或用了 FP8 权重但引擎按 BF16 解析。检查 TP/EP 与 config.json 的整除关系。
5. 启动后显存瞬间 OOM
max_model_len 填了模型上限,KV cache 预留把显存吃光。先降到业务真实需要,再考虑加卡或加量化。
6. 多卡启动卡在 NCCL 初始化
网卡没指定。加 export NCCL_SOCKET_IFNAME=<网卡名>、export NCCL_IB_DISABLE=1(无 RDMA 时)再试;用 ip addr 确认网卡名。
7. 单条请求正常,并发一上来就乱码或重复
并行通信或 KV cache 分页有 bug,或 block_size 这类参数被设成了不支持的值。回到单请求、单卡复现,再逐步加回并行。
8. 压测数据忽高忽低
没预热、机器上有其他进程抢卡、或者 GPU 温度撞墙降频。压测期间用 nvidia-smi -l 1 盯着,把其他任务全部清掉。
9. 吞吐数字漂亮但用户体验差
只看了 tokens/s,没看 TTFT。高并发下吞吐上去了、首字延迟爆炸,聊天场景完全不可用。两个指标必须一起报。
---
下一步建议
先把基线固化下来。把压测脚本、启动参数、环境变量写进一个 run.sh 和一份 bench.env,签进 git。之后每次换引擎版本或调参,跑同一条命令就能对比,不用回忆上次是怎么配的。
再试量化。BF16 显存吃紧时,考虑 FP8 或 INT8 权重量化,记录精度损失(拿一组固定问题做人工或自动比对)与吞吐收益的权衡。
然后是长上下文与 MoE 专项。如果业务场景是长文档或高并发对话,重点测 8K/32K 上下文下的表现,以及专家并行度对显存和吞吐的影响——这正是 DeepSeek 系列引擎设计的强项所在。
最后考虑补齐服务层。单机跑通之后,前面的网关、限流、多副本路由、监控(TTFT、TPOT、队列长度、KV cache 命中率)才是生产化的关键。推理引擎只是其中一环,别在选型上纠结太久,先把可观测性做出来,数据会告诉你答案。
