跳到主内容
快讯直播
AI智模界
教程

上手 DeepSeek 开源推理引擎:编译、权重加载与吞吐压测

这篇能做出什么

按下面的流程走完,你会得到三样东西:

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_sizeKV 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 命中率)才是生产化的关键。推理引擎只是其中一环,别在选型上纠结太久,先把可观测性做出来,数据会告诉你答案。

AI 生成本文由 AI 基于公开信息自动生成,仅供参考。