用两台 GB10 跑 DeepSeek-V4-Flash:284B 模型的双机部署记录
目录
我手上有两台 DGX Spark——GB10(Blackwell)芯片,每台 128GB 统一内存。2026-05-31 这一轮里,我给自己定的目标很直接:把 DeepSeek-V4-Flash(284B 总参 / 13B 激活的 MoE,官方 FP8,原生 1M 上下文 + MTP)完整地跑起来,尽量把这两台机器都用起来。
这篇文章不是一份通用安装手册,更像是我把这轮的环境、约束和几个关键点记清楚的一次实践整理:为什么一台机器装不下这个模型、GB10 该选哪个推理引擎、从源码构建时踩到的一个隐蔽 torch 问题、权重为什么最好走本地路径,以及双机启动之后怎么调到我自己能接受的吞吐。
实验环境#
| 项目 | 值 |
|---|---|
| 测试日期 | 2026-05-31 |
| 节点 | 2 x DGX Spark(GB10),每台 128GB 统一内存 |
| 拓扑 | 双机 TP=2,节点间 200Gbps CX7 / RoCE |
| 模型 | DeepSeek-V4-Flash,官方 FP8,46 shards,约 149GB |
| vLLM 路径 | jasl/vllm 分支 codex/ds4-sm120-min-enable |
| 工具链 | eugr/spark-vllm-docker |
下面提到的参数、吞吐和踩坑,都只对应我这次这套环境。如果你在更晚的时间参考这篇记录,我会更建议把上游 branch 再固定到你自己验证过的 commit 或镜像 tag。
为什么必须两台机器#
先算一笔账。按我这次拿到的 DeepSeek-V4-Flash 官方 FP8 权重目录来看,它是 46 个 shard、约 149GB。一台 GB10 的统一内存是 128GB——光是权重就装不下,更别说还要留给 KV cache 和激活值。
所以对我这次这套硬件来说,这不是「为了更快」才上多机,而是模型尺寸让我不得不把权重切开。两台机器做 张量并行(TP=2),每个节点扛大约 74GB 权重,在 gpu-memory-utilization 0.85(约 108GB 可用)下还能留出空间给 KV cache。两台 GB10 加起来的 256GB 统一内存,才比较接近把这个 284B 模型连同长上下文一起容纳下来的下限。
GB10 的内存是 CPU/GPU 共享的一整块 LPDDR5X,带宽约 273 GB/s——这既是它能用相对低的成本装下大模型的原因,也是单流吞吐的天花板所在。两个节点之间靠一条 200Gbps 的 CX7 链路(RoCE)做 TP 的 NCCL 通信,这条内网链路的延迟直接决定了双机方案是否划算。
引擎:为 GB10 选对 vLLM#
GB10 是 sm_121 计算能力,属于比较新的芯片。对我这次这套环境来说,有个绕不开的现实:社区里现成的推理引擎二进制,要么还没支持 V4 这个模型结构,要么没有为 sm_121 预编译对应的 kernel。
具体卡点在 V4 的稀疏 MLA 注意力上——它默认走的那条 kernel 路径在 GB10 上没有可用的预编译实现。我这次用的解决办法是 jasl 的 vLLM fork(分支 codex/ds4-sm120-min-enable),它做了两件关键的事:
- 补上了 GB10
sm_121的支持; - 提供一个开关
VLLM_TRITON_MLA_SPARSE=1,把稀疏 MLA 换成 Triton 写的实现,绕开缺失的预编译 kernel。
至少在我这次构建里,这个 env 基本决定了能不能跑起来,而且它是运行时开关,不是只在 build 阶段生效。
因为没有现成镜像可用(而且这些机器在国内,外部镜像仓库基本拉不动),只能从源码构建。我用的是 eugr 的 spark-vllm-docker 工具链,它把「构建镜像 → 拷到另一台」这套流程封装好了:
cd ~/spark-vllm-docker
./build-and-copy.sh \
--vllm-repo https://github.com/jasl/vllm.git \
--vllm-ref codex/ds4-sm120-min-enable --rebuild-vllm \
-t vllm-node-dsv4 --copy-to 192.168.200.102
构建要 clone GitHub,所以构建前得先把外网代理拉起来。整个从源码编译在我这轮大约 40 多分钟,产物镜像通过 200G 内网链路自动同步到第二台。
构建踩的坑:torch 被换成了 CPU 版#
镜像构建「成功」之后,容器一跑 vLLM 就报错:
ImportError: libtorch_cuda.so: cannot open shared object file
vllm._C 这个 C++ 扩展加载不了。排查下来根因有点反直觉:至少在我这次这套镜像和依赖解算结果里,runner 阶段安装 vllm wheel 以及 ray / fastsafetensors 这些依赖时,会顺手把镜像里原本的 cu130 版 torch 覆盖成 CPU wheel(我看到的是 2.x+cpu 这一类版本)。CPU 版 torch 自然没有 libtorch_cuda.so,于是带 CUDA 的 C++ 扩展全部失效。
修复方式是在构建完成的容器里把匹配 CUDA 13.0 的 torch 重新装回去,先确认 torch.version.cuda 回来了,再验证 vllm._C 能正常 import,然后 docker commit 固化进镜像,再把修好的镜像拷到第二台:
docker exec <ctr> uv pip install torch==2.11.0+cu130 \
--index-url https://download.pytorch.org/whl/cu130
docker exec <ctr> python -c "import torch; print(torch.__version__, torch.version.cuda)"
docker exec <ctr> python -c "import vllm._C; print('ok')"
docker commit <ctr> vllm-node-dsv4:latest
这一步如果漏掉,后面双机很可能起不来——而且报错信息(缺 .so)和真正的原因(依赖装包时悄悄把 torch 降级)隔得挺远,是我这次最想记下来的一个坑。
权重:用本地路径,别用 repo-id#
模型权重我从国内镜像源下到两台机器的同一个缓存目录里(46 shard / 149GB 的扁平目录结构)。
启动时我这里有个细节我会显式处理:vllm serve 的模型参数直接写容器内的本地路径,而不是 deepseek-ai/DeepSeek-V4-Flash 这样的 HuggingFace repo-id。
# 对:容器内本地路径
/root/.cache/huggingface/hub/DeepSeek-V4-Flash
# 错:repo-id —— worker 节点没有外网代理,会去解析/重下,直接失败
deepseek-ai/DeepSeek-V4-Flash
原因有两个:worker 节点(第二台)没有外网代理,传 repo-id 会触发 HF 的在线解析、要么报错要么想重新下载;另外在我这个挂载方式下,如果 HF 缓存里的软链接被写成绝对路径,挂进容器后就会解析不到。直接指本地目录可以一次性绕开这两个问题。
双机启动与 MTP 调优#
镜像和权重都就位后,用工具链的 recipe 一条命令把两台一起拉起来(--no-ray 表示不依赖 Ray,直接用 vLLM 自己的多进程后端跨节点):
./run-recipe.sh deepseek-v4-flash --no-ray
我最后稳定跑通时,两台上展开的核心参数大致是这样:
export VLLM_TRITON_MLA_SPARSE=1
export NCCL_IB_DISABLE=0
vllm serve /root/.cache/huggingface/hub/DeepSeek-V4-Flash \
--served-model-name deepseek-v4-flash \
--tensor-parallel-size 2 \
--gpu-memory-utilization 0.85 \
--kv-cache-dtype fp8 --block-size 256 \
--max-model-len 1000000 \
--distributed-executor-backend mp \
--compilation-config '{"cudagraph_mode":"FULL_AND_PIECEWISE"}' \
--speculative-config '{"method":"deepseek_mtp","num_speculative_tokens":2}'
几个值得说明的点:
--tensor-parallel-size 2:权重跨两台切开,前面算过,对我这次这套机器来说是必须的。- MTP(
deepseek_mtp,num_speculative_tokens=2):V4-Flash 自带多 token 预测(Multi-Token Prediction)头。按我这次单流、warm 的 decode 测法,开了之后吞吐从约 25 tok/s 提到约 42 tok/s。这个数字只代表这个 profile 下的单流结果,不等于并发总吞吐。 cudagraph_mode = FULL_AND_PIECEWISE:这个 fork(jasl 的 vLLM 分支)在 GB10 上走的 CUDA graph 路径,捕获后稳定吞吐更高。--max-model-len 1000000:这里指配置上限。我这篇里记录的是服务在 1M 配置下能稳定启动并保留可用吞吐,不是每次请求都实际做了 1M prefill。NCCL_IB_DISABLE=0:让 TP 通信走那条 200G RoCE 链路,而不是退回普通以太网。- 统一内存的两个关键约束:
gpu-memory-utilization压在 0.85、并且关掉 swap(swapoff -a)。再往上调内存利用率,在这块共享内存的架构上很容易把整机(包括 sshd)一起拖进 OOM 卡死。
结果#
跑通之后的状态:
| 指标 | 值 |
|---|---|
| 模型 | DeepSeek-V4-Flash(284B / 13B-active,官方 FP8) |
| 拓扑 | 双机 TP=2,200G RoCE 互联 |
| 单流 decode 吞吐(warm,开 MTP) | 约 42 tok/s |
| 上下文配置上限 | 1M |
| 每节点权重 | 约 74GB(gpu-memory-utilization 0.85) |
服务以 OpenAI 兼容接口暴露在第一台的 :8000,模型名 deepseek-v4-flash,并且支持 /v1/chat/completions 与 /v1/responses 两套接口。日常运维我把它收进了几个 make 目标里——v4flash-run / status / test / logs / stop——重启机器后一条命令就能把双机重新拉起来。
这里的 42 tok/s 是我这次单流、warm、num_speculative_tokens=2 下看到的 decode 吞吐;1M 也是配置上限,不是一次把 prompt 真打满到 1M 的压测结果。如果你要把它当成严格 benchmark,我会更建议另外把 prompt/output token 数、warmup 轮次和并发数单独记出来。
总结#
回头看,对我这次这套环境,真正决定能不能稳定跑起来的主要是三件事:先承认单机内存装不下,稳妥地双机切权重;为 GB10 选对带 sm_121 支持的引擎,并在运行时显式打开 Triton 稀疏 MLA;以及把源码构建里那个会把 torch 换成 CPU wheel 的依赖坑提前处理掉。把这三件事理顺,284B 的模型就能比较稳定地跑在两台 GB10 上。
后续方向#
目前这套部署跑通了单流推理,后续可以继续探索的方向包括:多并发请求下的吞吐与排队延迟表现、更长上下文(接近 1M)的实际 prefill 耗时、以及是否可以通过 CPU offloading 或投机解码进一步压榨双机的利用率。