Mopheus 一体机双机部署 DeepSeek 实战:双线 RoCE 互联与 Claude Code、Kimi Code 接入
一、本地环境与部署结构
二、先了解几个部署中会用到的概念
三、双线接线与接口对应关系
四、部署前检查
五、配置四个 RoCE 互联子网
六、验证 IP、MTU 和 RDMA 连通性
七、准备模型与部署工程
八、构建带编码器修正的运行镜像
九、配置双机运行参数
为 API 配置访问密钥
十、启动双机 DeepSeek
十一、查看模型加载与初始化日志
十二、验证 API 与流式输出
十三、配置 Claude Code 访问本地 DeepSeek
1. 先测试 Messages 接口
2. 修改 settings.json
3. 发起一次实际请求
十四、配置 Kimi Code 访问本地 DeepSeek
1. 查看版本并备份配置
2. 增加本地 provider 和模型
3. 启动 Kimi 并验证
十五、让 Mopheus 平台读取新的客户端配置
十六、实施过程中容易遇到的问题
十七、使用脚本进行单线故障切换
1. 准备脚本与运行目录
2. 查看当前配置和接口状态
3. 线 1 故障,切换到线 0
4. 线 0 故障,切换到线 1
5. 线缆修复后,恢复双线
6. 脚本执行了哪些操作
7. 保存切换日志和退出码
上一篇文章聊了 Mopheus 一体机的双机互联,也把两个高速口、一根线和两根线的关系梳理了一遍。现在两根 DAC 线也准备好了,接下来就进入实际部署:让这两台一体机一起跑起来 DeepSeek。
平时交流中,我们习惯把这两个高速口叫作“IB 口”。这套环境实际使用的是 Ethernet/RoCE,进入 Linux 后,两根线对应四组 Ethernet/RoCE 接口。理解了这层关系,接下来的 IP 配置、NCCL 选路,以及断线后的恢复操作就容易对应起来了。
这次使用 mopheus-ufo01 和 mopheus-ufo02 两台 GB10 主机,采用双线直连、TP=2 的方式部署 DeepSeek。先把模型服务启动并验证,再给 Claude Code、Kimi Code 配置本地模型入口,最后接入 Mopheus 平台。
下面按操作顺序记录整个过程:环境检查、网络配置、模型与镜像准备、双机启动、接口测试,以及客户端接入。网络部分保留部署时用到的接口表和检查命令,方便一边操作一边核对。文中的 IP、目录和账户按自己的环境替换即可。
一、本地环境与部署结构
本例两台 Mopheus 一体机均采用 NVIDIA GB10 Grace Blackwell SoC。每台包含 20 核 Arm CPU 和 Blackwell GPU,配备 128GB CPU/GPU 统一系统内存。平台规格可参考 NVIDIA 硬件说明,具体整机配置以 Mopheus 设备清单为准。
我们把两台机器分别安排为 Head 和 Worker:
项目 | ufo01 | ufo02 |
|---|---|---|
主机名 | mopheus-ufo01 | mopheus-ufo02 |
管理地址 | 192.168.31.25 | 192.168.31.27 |
分布式角色 | Head / Rank 0 | Worker / Rank 1 |
主要职责 | 接收请求、调度并参与计算 | 参与计算 |
本例对外 API | 8100 端口 | headless 模式 |
这里的 TP=2 是张量并行配置:两个计算 Rank 协作完成模型计算。支持切分的张量按框架策略分配,部分张量或辅助模块可能在两端保留副本。
两台机器的标称内存合计为 256GB,但每台仍有自己的内存空间。模型能够跨节点运行,是因为框架负责分片和通信。实际可用容量还要考虑操作系统、运行时、缓存和其他进程的占用。
客户端最终连接 Head 的地址:
http://192.168.31.25:8100/v1
Head 同时承担计算和服务入口,因此它是整个部署中的关键节点。第二根高速线主要增加路径选择与恢复条件,整机冗余需要另行设计。
二、先了解几个部署中会用到的概念
客户端提交一段文字之后,服务内部大致经历两个阶段。
Prefill 负责处理输入内容,建立后续生成需要的状态。Decode 则基于已有上下文继续生成输出。两个 Rank 在各自计算的同时,按执行计划进行跨节点通信。
从请求进入服务到输出答案,可以按下面的时序理解:
图中展示的是阶段与职责,实际 collective 次数和类型由模型执行计划决定。
几个经常出现在日志里的名词,也可以在这个过程中理解:
名词 | 可以怎样理解 |
|---|---|
KV Cache | 保存生成过程会复用的中间状态,减少重复计算 |
Prefix Cache | 在条件满足时复用相同前缀的计算结果 |
MoE | 根据输入选择部分专家参与计算;模型仍需存储相应权重 |
NCCL | 为 GPU 进程提供集合通信的运行库 |
RoCE | 在以太网上承载 RDMA 通信的技术 |
DSpark 推测解码 | 先提出候选 token,再由目标模型验证并接受合适的结果 |
这里设置 SPEC_TOKENS=5。这个数字描述本次候选 token 数量配置;token 是分词单位,接受多少候选由运行过程决定。对应实现和编码语义需要与模型版本匹配。DeepSeek 模型说明
还有一个经常被误解的端口:25000。
本部署通过 Head 的 10.10.1.25:25000 完成分布式初始化与协调。进程组建立后,集合通信使用运行库选定的 transport,框架也可能建立其他控制连接。因此,网络实施要同时考虑初始化连接、框架控制通信和 RDMA 设备可达性。
三、双线接线与接口对应关系
先把两根线命名清楚。本文统一按物理口编号称呼:口 0 接线 0,口 1 接线 1。
本例 ConnectX-7 经两条 PCIe Gen5 x4 链路连接 SoC,每个 QSFP 物理口对应两组 Ethernet/RoCE 接口。因此,两根线接好以后,每台主机需要核对四个 Ethernet 接口和四个 RoCE device。官方接口映射说明
物理线 | Ethernet 接口 | RoCE device | ufo01 地址 | ufo02 地址 |
|---|---|---|---|---|
线 0 | enp1s0f0np0 | rocep1s0f0 | 10.10.1.25/24 | 10.10.1.27/24 |
线 0 | enP2p1s0f0np0 | roceP2p1s0f0 | 10.10.3.25/24 | 10.10.3.27/24 |
线 1 | enp1s0f1np1 | rocep1s0f1 | 10.10.2.25/24 | 10.10.2.27/24 |
线 1 | enP2p1s0f1np1 | roceP2p1s0f1 | 10.10.4.25/24 | 10.10.4.27/24 |
把上面的地址对应到两根物理线,连接关系如下:
其中大写的 P 是接口名称的一部分。记录接口时保留完整名称,后面的 IP、GID 和 NCCL 配置都依赖这张对应表。
两根线提供两条物理连接,但它们仍共享本机 NIC 及上游资源。官方单线双机连接方案已经是完整的互联基线;双线手工配置的价值在于增加可选路径和故障处置条件,端口标称速率相加不代表应用拥有 400Gb/s 有效总带宽。官方双机连接步骤
在两台节点分别执行:
hostname
ip -br link
ibdev2netdev
rdma link show
lspci -D | grep -i -E 'Mellanox|NVIDIA|Ethernet'
再核对线缆标签、设备兼容性和物理口位置。已有分布式任务运行时,接线或换线安排在维护窗口内完成。
检查结果: 实物线缆、物理口、Ethernet 名称、RoCE 名称和 PCIe 地址能够一一对应。
四、部署前检查
网络配置之前,先确认系统能支持这套部署。以下 Linux 命令在两台 Mopheus 节点分别执行;文中带 ssh ufo01、ssh ufo02 的命令则在已配置 SSH 别名的管理终端执行。
uname -m
cat /etc/os-release
nvidia-smi
docker version
docker compose version
ibv_devinfo
free -h
df -h /home/models /home/mopheus
需要具备匹配 GB10 的驱动、NVIDIA Container Toolkit、Docker Compose、RDMA 用户态工具,以及网络配置工具。两端采用经过验证的系统、驱动和容器组合,部署过程中保持这套组合稳定。
如果主机已经运行其他大模型,先确认所属业务和维护安排,再停止需要替换的服务。例如这里以旧容器 qwen36-nvfp4 为例:
docker ps -a --format 'table {{.Names}}\t{{.Status}}\t{{.Image}}'
# 仅在确认该旧服务可停止且容器存在时执行
docker stop qwen36-nvfp4
保留旧容器配置,有助于后续恢复;同时检查是否有守护程序会再次自动拉起旧服务。
GB10 使用统一内存,资源观察应结合 free -h、进程信息和模型初始化日志。某些 nvidia-smi 汇总字段可能显示 Not Supported,这表示该字段的显示支持情况。NVIDIA 相关说明
检查结果: GPU、容器与 RDMA 工具可用,模型和缓存目录空间充足,旧业务的资源使用与恢复安排明确。
五、配置四个 RoCE 互联子网
四个独立子网让地址、路由和设备更容易核对。实施前确认 10.10.1.0/24 至 10.10.4.0/24 与现有 LAN、VPN、容器网络没有冲突。高速直连接口只配置本地地址,管理网络继续承担默认路由。
先在两端备份网络配置:
sudo install -d -m 700 /root/roce-config-backup
sudo cp -a /etc/netplan \
/root/roce-config-backup/netplan-$(date +%Y%m%d-%H%M%S)
检查现有 Netplan 文件,整理同名接口的重复定义,并确认实际 renderer。下面使用 systemd-networkd 配置;采用 NetworkManager 的环境应按其现有管理方式调整。
在 ufo01 设置 ROCE_HOST=25;在 ufo02 设置 ROCE_HOST=27,然后分别执行:
ROCE_HOST=25 # ufo02 改为 27
sudo tee /etc/netplan/60-roce-interconnect.yaml >/dev/null <<EOF
network:
version: 2
ethernets:
enp1s0f0np0:
renderer: networkd
dhcp4: false
dhcp6: false
addresses: [10.10.1.${ROCE_HOST}/24]
mtu: 9000
optional: true
enp1s0f1np1:
renderer: networkd
dhcp4: false
dhcp6: false
addresses: [10.10.2.${ROCE_HOST}/24]
mtu: 9000
optional: true
enP2p1s0f0np0:
renderer: networkd
dhcp4: false
dhcp6: false
addresses: [10.10.3.${ROCE_HOST}/24]
mtu: 9000
optional: true
enP2p1s0f1np1:
renderer: networkd
dhcp4: false
dhcp6: false
addresses: [10.10.4.${ROCE_HOST}/24]
mtu: 9000
optional: true
EOF
sudo chmod 600 /etc/netplan/60-roce-interconnect.yaml
sudo netplan generate
sudo netplan try --timeout 120
通过独立管理连接或本地控制台操作,在倒计时内验证地址和连接,再接受配置。
接着给四个高速接口设置 ARP 参数。备份已有同名文件后,按以下方式配置:
if [ -f /etc/sysctl.d/99-roce-arp.conf ]; then
sudo cp -a /etc/sysctl.d/99-roce-arp.conf \
/root/roce-config-backup/99-roce-arp.conf.$(date +%Y%m%d-%H%M%S)
fi
{
for dev in enp1s0f0np0 enp1s0f1np1 enP2p1s0f0np0 enP2p1s0f1np1; do
echo "net.ipv4.conf.$dev.arp_ignore=1"
echo "net.ipv4.conf.$dev.arp_announce=2"
done
} | sudo tee /etc/sysctl.d/99-roce-arp.conf >/dev/null
sudo sysctl -p /etc/sysctl.d/99-roce-arp.conf
它们用于约束 ARP 应答与源地址选择,作用于网络地址行为。链路故障恢复还需要后面的分布式服务重建流程。
六、验证 IP、MTU 和 RDMA 连通性
先验证四个子网的地址、路由和 MTU。
在 ufo01 执行:
for n in 1 2 3 4; do
ip route get 10.10.${n}.27 from 10.10.${n}.25
ping -c 2 -W 3 -M do -s 8972 -I 10.10.${n}.25 10.10.${n}.27
done
在 ufo02 执行反向检查:
for n in 1 2 3 4; do
ip route get 10.10.${n}.25 from 10.10.${n}.27
ping -c 2 -W 3 -M do -s 8972 -I 10.10.${n}.27 10.10.${n}.25
done
这里指定了源地址,便于确认每一组路径。8972 字节数据加常规 IPv4/ICMP 头构成 9000 字节 IP 包;-M do 要求禁止分片。
接着在两端检查 RDMA:
ibdev2netdev
ibv_devinfo
show_gids
每个 RoCE device 应为 ACTIVE,并具有与对应 IPv4 地址匹配的 RoCE v2 GID。GID 可以理解为 RDMA 通信使用的地址标识,索引可能随系统地址配置改变,现场逐项记录即可。
需要进一步验证时,可以用 ibv_rc_pingpong 做少量数据交换。以下以 rocep1s0f0 为例,先在各自节点把 GID_INDEX 设置为查询所得的索引。
# ufo02:先启动服务端
ibv_rc_pingpong -d rocep1s0f0 -i 1 -g "$GID_INDEX" \
-p 18515 -s 64 -n 10
# ufo01:再启动客户端
ibv_rc_pingpong -d rocep1s0f0 -i 1 -g "$GID_INDEX" \
-p 18515 -s 64 -n 10 10.10.1.27
按接口表替换设备和地址,验证其余三组,再交换角色检查反向。这里关注正常完成和有无 CQ/QP 错误,这里以交换完成、没有通信错误为通过条件。
检查结果: 四组双向 IP/MTU 检查通过,RoCE 地址映射正确,所执行的 RDMA 交换正常结束。
七、准备模型与部署工程
双机部署需要三类资产,它们各自解决不同问题:
资产 | 本例选择 | 作用 |
|---|---|---|
模型 checkpoint | deepseek-ai/DeepSeek-V4-Flash-0731 | 提供权重、配置、分词器等模型文件 |
部署工程 | 0rand 社区 ServingStack | 提供 Compose、配置模板与编码器修正流程 |
基础镜像 | aidendle94/sparkrun-vllm-ds4-gb10:production-3.75 | 提供特定的推理运行环境 |
模型、社区工程和镜像分别记录来源与版本。镜像中的 production 是发布者采用的标签名称,本文将使用的内容固定下来,再验证是否适合这套环境。
在管理终端取得部署工程,并记录 commit:
git clone \
https://github.com/0rand/DeepSeek-v4-DSpark-Aidendle94-GB10-ServingStack \
DS4F-0731-Aiden-3.75
cd DS4F-0731-Aiden-3.75
git rev-parse HEAD
取得工程后,核对 README、Compose、Dockerfile 和 docs/ENCODER-PATCH.md,冻结本次选定版本。使用离线包时,按归档校验值和包内清单完成同样的核对。
模型也固定到完整 commit SHA。以下命令在安装了 hf 工具的节点 Bash 中执行:
read -r -p '模型完整 commit SHA: ' MODEL_COMMIT
[[ "$MODEL_COMMIT" =~ ^[0-9a-f]{40}$ ]] || exit 1
mkdir -p /home/models/DeepSeek-V4-Flash-0731
hf download deepseek-ai/DeepSeek-V4-Flash-0731 \
--revision "$MODEL_COMMIT" \
--local-dir /home/models/DeepSeek-V4-Flash-0731
printf '%s\n' "$MODEL_COMMIT" \
> /home/models/DeepSeek-V4-Flash-0731.revision.txt
模型可以下载一次,再把完整目录复制到另一节点。两端生成并比较文件清单:
cd /home/models/DeepSeek-V4-Flash-0731
find . -type f ! -path './.cache/*' -print0 | sort -z | \
xargs -0 sha256sum > ../DeepSeek-V4-Flash-0731.sha256
这次环境记录的 48 个分片、约 155.43GiB 是该批模型文件的资产记录。接收另一批物料时,以对应 revision 的完整清单为准。
检查结果: 模型来源明确、两端文件一致,工程版本固定,模型和镜像均有可追溯记录。
八、构建带编码器修正的运行镜像
这里单独记录一下 reasoning-effort 编码器的处理。
所选社区工程的补丁说明指出,基础镜像与 0731 模型的 reasoning-effort 编码语义存在需要修正的差异。它涉及请求参数怎样转换成模型输入,这里把修正直接构建进运行镜像。社区编码器补丁说明
先在两端准备工作和缓存目录:
mkdir -p /home/mopheus/dockers/DS4F-0731-Aiden-3.75
mkdir -p /home/mopheus/.cache/vllm-ds4-0731-aiden
mkdir -p /home/mopheus/.cache/tilelang-ds4-0731-aiden
从管理终端、工程父目录同步所选版本:
rsync -a DS4F-0731-Aiden-3.75/ \
ufo01:/home/mopheus/dockers/DS4F-0731-Aiden-3.75/
rsync -a DS4F-0731-Aiden-3.75/ \
ufo02:/home/mopheus/dockers/DS4F-0731-Aiden-3.75/
在 ufo01 获取选定基础镜像,记录平台、ID 和 digest,然后按所选工程构建:
docker pull aidendle94/sparkrun-vllm-ds4-gb10:production-3.75
docker image inspect aidendle94/sparkrun-vllm-ds4-gb10:production-3.75 \
--format 'Platform={{.Os}}/{{.Architecture}} ID={{.Id}} Digests={{json .RepoDigests}}'
cd /home/mopheus/dockers/DS4F-0731-Aiden-3.75
bash ./apply-encoder-patch.sh build mopheus-local/ds4f:3.75-0731-encoderfix
mopheus-local/ds4f:3.75-0731-encoderfix 是这里定义的本地镜像标签。核对补丁文件是否正确进入镜像:
docker run --rm --entrypoint sh mopheus-local/ds4f:3.75-0731-encoderfix \
-c 'sha256sum /opt/venv/lib/python3.12/site-packages/vllm/tokenizers/deepseek_v4.py /opt/venv/lib/python3.12/site-packages/vllm/tokenizers/deepseek_v4_encoding.py'
sha256sum deepseek_v4.py deepseek_v4_encoding.py
模块路径按冻结工程的补丁脚本核对。文件摘要一致之后,还要执行对应模型的编码测试用例,检查 low/high/max 的语义。已经包含修正且完成语义验证的镜像,记录其镜像指纹和验证结果即可。
将同一份运行镜像导出,再复制到 Worker 导入:
# ufo01
docker save -o /home/mopheus/ds4f-encoderfix.tar \
mopheus-local/ds4f:3.75-0731-encoderfix
sha256sum /home/mopheus/ds4f-encoderfix.tar
归档经受控传输到 ufo02 后:
# ufo02:先比较归档摘要,再导入
sha256sum /home/mopheus/ds4f-encoderfix.tar
docker load -i /home/mopheus/ds4f-encoderfix.tar
docker image inspect mopheus-local/ds4f:3.75-0731-encoderfix \
--format '{{.Id}}'
离线环境使用预先封版的镜像与模型归档完成这些步骤,并一并准备所需系统依赖。构建和下载在具备相应条件的准备环境完成。
九、配置双机运行参数
两端都在工作目录创建 .env,使用相同模型、镜像和网络参数。Rank 和节点自身地址由对应的 Head、Worker Compose 分别设置。
下面是这套手工部署方案的配置,其中模型 revision 占位符要替换为 第七节 记录的 SHA:
IMAGE=mopheus-local/ds4f:3.75-0731-encoderfix
MODEL_PATH=/root/.cache/huggingface/DeepSeek-V4-Flash-0731
MODEL_REVISION=REPLACE_WITH_VERIFIED_MODEL_COMMIT_SHA
SERVED_MODEL_NAME=deepseek-v4-flash
HF_CACHE=/home/models
VLLM_CACHE=/home/mopheus/.cache/vllm-ds4-0731-aiden
TILELANG_CACHE=/home/mopheus/.cache/tilelang-ds4-0731-aiden
NCCL_IB_HCA=rocep1s0f0,rocep1s0f1,roceP2p1s0f0,roceP2p1s0f1
NCCL_SOCKET_IFNAME=enp1s0f0np0,enp1s0f1np1,enP2p1s0f0np0,enP2p1s0f1np1
CONTROL_IF=enp1s0f0np0
MASTER_ADDR=10.10.1.25
HEAD_ROCE_IP=10.10.1.25
WORKER_ROCE_IP=10.10.1.27
WORKER_SSH_TARGET=mopheus@192.168.31.27
WORKER_DIR=/home/mopheus/dockers/DS4F-0731-Aiden-3.75
PORT=8100
TP_SIZE=2
SPEC_TOKENS=5
TEMPERATURE=0.8
TOP_P=0.25
REPETITION_PENALTY=1.0
GPU_MEMORY_UTILIZATION=0.84
MAX_MODEL_LEN=1048576
MAX_NUM_SEQS=16
MAX_NUM_BATCHED_TOKENS=16384
GRAPH_CAP=512
ASYNC_SCHED=1
OMP_NUM_THREADS=4
宿主机 /home/models 挂载到容器 /root/.cache/huggingface,所以配置中的模型路径采用容器视角。核对 Compose 时,同时检查 GPU reservation、/dev/infiniband、host networking、memlock 和缓存挂载。
几个网络字段也各有用途:
- NCCL_IB_HCA 给出 RDMA 候选设备。
- NCCL_SOCKET_IFNAME 选择 socket 接口。
- CONTROL_IF 是本社区部署栈使用的控制接口变量。
- MASTER_ADDR 指向分布式初始化地址。
所用启动脚本的 GID 自动选择逻辑,会把找到的首个 IPv4 RoCE v2 索引用作全局索引。因此,启动前要核对本节点四设备在该索引下都匹配预期地址;存在差异时先处理选择逻辑,再启动。
资源字段是这套配置的起点。MAX_MODEL_LEN 表示服务声明上限,实际可容纳的请求受 KV Cache 和可用内存约束。首次验收使用短请求,初始化若报告空间不足,再同步调整两端资源参数。
为观察 NCCL 建链,在两端新增 compose.site.yaml:
services:
ds4:
environment:
NCCL_NET: IB
NCCL_IB_DISABLE: "0"
NCCL_DEBUG: INFO
NCCL_DEBUG_SUBSYS: INIT,NET,GRAPH
OMP_NUM_THREADS: "${OMP_NUM_THREADS:-4}"
服务名 ds4 需与冻结的 Compose 一致。.env 提供变量插值,environment 才决定哪些字段传入容器,后续命令统一带上这份覆盖文件。
为 API 配置访问密钥
在新环境的 Head 工作目录生成后端 Key,写入 .env,再把最终配置同步到 Worker。已有服务继续使用原来的 Key,避免影响现有客户端。
python3 - <<'PYKEY'
from pathlib import Path
import secrets
p = Path('.env')
lines = [x for x in p.read_text().splitlines() if not x.startswith('API_KEY=')]
lines.append('API_KEY=' + secrets.token_urlsafe(32))
p.write_text('\n'.join(lines) + '\n')
p.chmod(0o600)
print('API_KEY 已写入 .env')
PYKEY
在两端 compose.site.yaml 的 environment 中增加这一行,与前面的 NCCL 配置放在一起:
API_KEY: "${API_KEY:?API_KEY must be set}"
然后在两份 Compose 的 vllm serve 命令参数中加入:
--api-key "$${API_KEY}"
这里使用双美元符号,让变量在容器内展开。后面所有访问都使用这个本地服务 Key。
十、启动双机 DeepSeek
在管理终端执行结构预检:
ssh ufo02 'cd /home/mopheus/dockers/DS4F-0731-Aiden-3.75 && docker compose --env-file .env -f compose.worker.yaml -f compose.site.yaml config --quiet'
ssh ufo01 'cd /home/mopheus/dockers/DS4F-0731-Aiden-3.75 && docker compose --env-file .env -f compose.head.yaml -f compose.site.yaml config --quiet'
在节点本地核对解析后的模型路径、镜像、Rank、地址和挂载,配置中所有占位符均应已替换;含凭据的展开结果保存在受控环境。
按照这套部署工程的启动顺序,先启动 Worker:
ssh ufo02 'cd /home/mopheus/dockers/DS4F-0731-Aiden-3.75 && docker compose --env-file .env -f compose.worker.yaml -f compose.site.yaml up -d'
ssh ufo02 'docker ps --filter name=ds4-dspark'
ssh ufo02 'docker logs --tail 60 ds4-dspark'
确认容器没有立即退出,稍后启动 Head。社区脚本约等待 15 秒,这是编排间隔,Worker 此时可能仍在等待另一端:
ssh ufo01 'cd /home/mopheus/dockers/DS4F-0731-Aiden-3.75 && docker compose --env-file .env -f compose.head.yaml -f compose.site.yaml up -d'
这个顺序是本部署栈的操作约定。Head 仍然是 MASTER_ADDR 指向的节点,Worker 在本例采用 headless 模式。
接下来分别观察两端:
ssh ufo01 'docker logs --tail 120 ds4-dspark'
ssh ufo02 'docker logs --tail 120 ds4-dspark'
十一、查看模型加载与初始化日志
启动日志可以分成五个阶段来读。
第一阶段:分布式初始化。 检查两个 Rank、world_size=2 和初始化地址。例如启动日志中可以核对以下字段:
world_size=2 rank=0 ... tcp://10.10.1.25:25000 backend=nccl
world_size=2 rank=1 ... tcp://10.10.1.25:25000 backend=nccl
第二阶段:通信路径建立。 两端执行:
docker exec ds4-dspark sh -lc 'ls /dev/infiniband; ls /sys/class/infiniband'
docker logs ds4-dspark 2>&1 | grep -E 'NCCL|NET/IB|NET/Socket|Channel|WARN|ERROR'
在本场景中,NET/IB 可用于表示 RoCE transport。初始化过程出现 socket 日志很正常,判断计算通信路径时,要结合该行日志的上下文、设备和 channel 信息。
四个设备列入候选、被运行库识别,以及实际承担某次请求的数据传输,是三个不同观察层次。按实际日志记录结果,就能清楚说明多路径配置发挥了什么作用。
第三阶段:权重和 DSpark 模块加载。 这批模型包含 48 个权重分片,以及下列日志类型:
Loading weights took ...
DSpark draft model loaded: 96 params
Model loading took 80.07 GiB and ... seconds
96 params 是该实现的加载日志计数,不宜理解成 96 个标量参数;80.07 GiB 是对应进程加载阶段记录的内存量。两端分别完成加载,才具备继续推进的条件。
第四阶段:编译、预热和 CUDA Graph 捕获。 TileLang、FlashInfer、DeepGEMM 等模块会准备运行所需的计算实现,日志可能出现:
TileLang begins to compile kernel ...
TileLang completes to compile kernel ...
DeepGEMM warmup: 100%
Capturing CUDA graphs ... 100%
编译缓存可按挂载配置落盘复用;CUDA Graph 则是进程内执行图,重启仍可能进行预热和图捕获。日志中的任务数和耗时用于识别阶段。
第五阶段:HTTP 服务就绪。 Head 出现 Application startup complete 后,进入接口验收。
检查结果: 两端初始化及模型准备完成,通信日志符合预期,Head API 启动,并且没有未处理的加载或运行错误。
十二、验证 API 与流式输出
先在 Head 查看健康状态:
curl --fail-with-body -sS --max-time 10 -o /dev/null \
-w '%{http_code}\n' http://127.0.0.1:8100/health
返回 200 后,继续检查模型接口。下面示例适用于启用了 Key 的后端,在 Head 的 Bash 中安全输入本地服务凭据:
read -r -s -p '本地服务 API Key: ' LOCAL_API_KEY
printf '\n'
curl --fail-with-body -sS --max-time 10 \
http://127.0.0.1:8100/v1/models \
-H "Authorization: Bearer ${LOCAL_API_KEY}"
这里使用 deepseek-v4-flash 和 ds4f-dspark 两个服务别名,它们可以指向同一个模型。核对返回的模型名与配置一致。
再发送一道便于判断结果的题目:
curl --fail-with-body -sS --max-time 180 \
http://127.0.0.1:8100/v1/chat/completions \
-H "Authorization: Bearer ${LOCAL_API_KEY}" \
-H 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"model": "deepseek-v4-flash",
"messages": [{"role": "user", "content": "请计算 12345 + 67890,仅回答结果。"}],
"max_tokens": 512,
"reasoning_effort": "low",
"stream": false
}
JSON
检查响应正文为 80235,并核对 finish_reason。stop 表示正常结束;length 表示达到输出上限,应结合正文判断并调整验证请求。
将同一请求的 stream 改为 true,并给 curl 增加 -N,即可验证 SSE 流式输出。观察 data: 事件与正常结束标志。SSE 分块与 tokenizer token 是不同概念,此处只做协议与功能验证。
完成后清除当前终端变量:
unset LOCAL_API_KEY
最后核对两端近期日志和进程状态,确认请求过程中没有新的 NCCL、CUDA 或加载错误。
十三、配置 Claude Code 访问本地 DeepSeek
下面在运行 Claude Code 的用户账户下操作,可以是管理电脑,也可以是 Mopheus 主机上的实际使用账户。先记录客户端版本:
claude --version
1. 先测试 Messages 接口
Claude Code 使用 Anthropic Messages 协议。这里以 http://192.168.31.25:8100 为接口地址示例,先测试该服务是否提供兼容端点;若返回 404,则使用提供 Messages 协议适配的本地网关地址,再继续配置。Claude Code 接到 DeepSeek 属于第三方兼容性实验,Anthropic 官方支持范围不包含非 Claude 模型。协议与支持范围说明
在 Bash 中输入对应入口的凭据,发出测试请求:
export ANTHROPIC_BASE_URL="http://192.168.31.25:8100"
read -r -s -p '本地 Messages 服务 Key: ' ANTHROPIC_AUTH_TOKEN
printf '\n'
export ANTHROPIC_AUTH_TOKEN
curl --fail-with-body -sS --max-time 180 \
"${ANTHROPIC_BASE_URL}/v1/messages" \
-H "Authorization: Bearer ${ANTHROPIC_AUTH_TOKEN}" \
-H 'anthropic-version: 2023-06-01' \
-H 'Content-Type: application/json' \
--data-binary @- <<'JSON'
{
"model": "deepseek-v4-flash",
"max_tokens": 512,
"messages": [{"role": "user", "content": "请计算 12345 + 67890,仅回答结果。"}]
}
JSON
检查 HTTP 成功,Messages 响应的 content 中包含答案 80235。这里检查的是协议兼容,Chat Completions 请求成功与 Messages 请求成功分别验证。
2. 修改 settings.json
先备份已有配置:
mkdir -p ~/.claude
if [ -f ~/.claude/settings.json ]; then
cp -a ~/.claude/settings.json \
~/.claude/settings.json.bak-$(date +%Y%m%d-%H%M%S)
fi
将下面字段合并到 ~/.claude/settings.json,保留已有其他设置。地址填刚才测试通过的 Messages 服务入口:
{
"env": {
"ANTHROPIC_BASE_URL": "http://192.168.31.25:8100",
"ANTHROPIC_AUTH_TOKEN": "REPLACE_WITH_LOCAL_MESSAGES_KEY",
"ANTHROPIC_MODEL": "deepseek-v4-flash"
}
}
替换 Key 占位符,然后检查 JSON:
chmod 600 ~/.claude/settings.json
python3 -m json.tool ~/.claude/settings.json >/dev/null
ANTHROPIC_AUTH_TOKEN 对应 Bearer 认证;入口若采用 x-api-key,则使用 ANTHROPIC_API_KEY 并同步修改测试请求头。这里的 BASE_URL 写服务根地址,客户端会拼接 Messages 路径。Claude Code 网关连接配置
3. 发起一次实际请求
claude -p '请计算 12345 + 67890,仅输出数字结果。'
预期答案:
80235
另开一个终端查看 Head 日志:
ssh ufo01 'docker logs --since 2m ds4-dspark 2>&1 | tail -n 80'
如果经过网关,同时检查网关请求日志。交互启动 claude 后,还可以使用 /status 核对实际 Base URL 与凭据来源。
短问答通过后,再在临时测试目录中验证文件读取、工具调用和流式输出。这样可以把“接口连接成功”和“编码工具的常用功能可用”分开确认。
十四、配置 Kimi Code 访问本地 DeepSeek
Kimi Code 在这里作为客户端,后端使用我们部署的 DeepSeek。它通过 OpenAI Chat Completions 协议连接,地址带 /v1。Kimi Code provider 配置说明
1. 查看版本并备份配置
kimi --version
mkdir -p ~/.kimi-code
if [ -f ~/.kimi-code/config.toml ]; then
cp -a ~/.kimi-code/config.toml \
~/.kimi-code/config.toml.bak-$(date +%Y%m%d-%H%M%S)
fi
下面使用 ~/.kimi-code/config.toml 的配置格式。较早的 kimi-cli 版本应先核对其实际配置目录和 provider 类型。
2. 增加本地 provider 和模型
编辑 ~/.kimi-code/config.toml,合并以下内容。default_model 放在 TOML 顶层;已有同名字段或表时直接修改对应项:
default_model = "local-deepseek"
[providers.local-vllm]
type = "openai"
base_url = "http://192.168.31.25:8100/v1"
api_key = "REPLACE_WITH_LOCAL_DEEPSEEK_KEY"
[models.local-deepseek]
provider = "local-vllm"
model = "deepseek-v4-flash"
max_context_size = 32768
这里有两个名称:local-deepseek 是客户端使用的配置别名,deepseek-v4-flash 是服务端注册的模型名。32768 是本例先采用的客户端上下文限制,后续按实际业务和服务容量调整。
替换 Key 后保存:
chmod 600 ~/.kimi-code/config.toml
使用 Python 3.11 及以上版本时,可以检查 TOML 语法:
python3 - <<'PYTOML'
import pathlib, tomllib
p = pathlib.Path.home() / '.kimi-code/config.toml'
with p.open('rb') as f:
config = tomllib.load(f)
assert config['models']['local-deepseek']['provider'] == 'local-vllm'
print('TOML 解析通过,本地模型映射存在')
PYTOML
3. 启动 Kimi 并验证
在同一用户账户启动:
kimi
发出测试问题:
请计算 12345 + 67890,仅回答结果。
核对答案 80235,再在 Head 查看对应请求记录:
ssh ufo01 'docker logs --since 2m ds4-dspark 2>&1 | tail -n 80'
出现 401 时先检查本地 Key;出现模型不存在时,对照 /v1/models 检查模型名。客户端返回答案且本地日志中有对应请求,才说明这次调用确实走了这两台 Mopheus。
十五、让 Mopheus 平台读取新的客户端配置
CLI 在终端测试通过后,再处理平台守护进程使用的配置。以下命令以实际运行 Mopheus 的用户执行:
systemctl --user cat mopheus-daemon.service
systemctl --user show mopheus-daemon.service -p EnvironmentFiles
先看 unit 是否配置了 EnvironmentFile=,找到真正被读取的文件。将其中旧的 ANTHROPIC_BASE_URL、模型名或其他冲突项改成这次使用的值,保留其余变量。
修改 .bashrc 前先备份:
cp -a ~/.bashrc ~/.bashrc.before-deepseek-$(date +%Y%m%d-%H%M%S)
用户级 Claude、Kimi 配置要放在平台运行账户的 home 下。root 下测试成功,并不意味着另一个用户启动的守护进程会自动读取 root 的配置。
完成相关修改后重启:
systemctl --user daemon-reload
systemctl --user restart mopheus-daemon
systemctl --user status mopheus-daemon --no-pager
journalctl --user -u mopheus-daemon -n 80 --no-pager
从 Mopheus 界面选择相应客户端与本地模型,发起同样的短问答,同时观察 Head 请求日志。测试顺序保持一致:API → CLI → 平台,哪一层开始出错,就优先检查这一层的地址、协议和配置来源。
十六、实施过程中容易遇到的问题
现象 | 排查方式 |
|---|---|
Linux 显示四组高速接口 | 按物理口、PCIe 地址与 RoCE 映射核对,两根线分别对应两组接口 |
Ping 正常,模型建链失败 | 继续检查 GID、RDMA 设备映射、NCCL transport 和两端实际容器环境 |
Worker 已启动,却没有 HTTP API | 本例 Worker 使用 headless,客户端访问 Head 的 8100 |
权重加载完成,API 还未就绪 | 查看 DSpark 模块、JIT、预热和图捕获等后续阶段 |
修改 .env 后行为没变 | 用对应 Compose 文件重建容器,使新变量进入运行环境 |
Claude Code 请求返回 404 | 核对入口是否提供 Messages 协议;需要时配置兼容适配层 |
Kimi 返回模型不存在 | 区分客户端别名 local-deepseek 与服务模型名 deepseek-v4-flash |
CLI 正常,Mopheus 平台异常 | 检查运行用户、配置目录、systemd 环境文件和覆盖变量 |
十七、使用脚本进行单线故障切换
双线接好以后,一条线故障时,可以通过另一条健康线重建双机服务。这里使用工程中的 scripts/switch_roce_link.sh 完成切换,配置同步、双机启停和问答检查都由它协调执行。
1. 准备脚本与运行目录
在 Head(ufo01)上,将工程中的脚本放到实际部署目录,与 .env、start.sh、stop.sh 和两份 Compose 文件放在一起:
/home/mopheus/dockers/DS4F-0731-Aiden-3.75/
├── .env
├── compose.head.yaml
├── compose.worker.yaml
├── start.sh
├── stop.sh
└── switch_roce_link.sh
脚本默认读取自身目录中的 .env。后面的命令均在 Head 的这个目录执行:
cd /home/mopheus/dockers/DS4F-0731-Aiden-3.75
chmod 750 switch_roce_link.sh start.sh stop.sh
这里把前面手工启动的方式衔接到脚本启停:配套 start.sh 直接调用两份 Compose,没有加载 compose.site.yaml。 使用脚本前,将第九节覆盖文件中的 environment 配置合并到 compose.head.yaml、compose.worker.yaml 的 ds4 服务中,保留已有环境项和 API Key 启动参数。后续由脚本统一启停,运行参数以这两份文件为准。
合并后检查两份配置:
docker compose --env-file .env -f compose.head.yaml config --quiet
docker compose --env-file .env -f compose.worker.yaml config --quiet
当前脚本按本例接口和地址编写,通过健康线的高速 IP 登录 Worker。先在 Head 验证两条 SSH 路径:
ssh -o BatchMode=yes -o ConnectTimeout=5 root@10.10.1.27 hostname
ssh -o BatchMode=yes -o ConnectTimeout=5 root@10.10.2.27 hostname
两条命令都应返回 Worker 主机名。这里使用脚本内置的 root 账户;换成其他账户或地址时,需同步修改脚本的模式映射,并确认该账户具备相应 Docker 和文件操作权限。
2. 查看当前配置和接口状态
./switch_roce_link.sh status
输出包括本地 .env 网络字段、两端接口 carrier,以及两端容器配置中的 NCCL_IB_HCA。配合下面的命令确认容器进程和 RDMA 状态:
docker ps -a --filter name=ds4-dspark
rdma link show
status 按当前 .env 中的 SSH 地址查询 Worker。当前控制线故障时,可能显示 Worker 不可达;实际执行 port0 或 port1 时,脚本会改用目标模式对应的 SSH 地址。
3. 线 1 故障,切换到线 0
暂停上层新请求,保存故障时的两端日志,然后执行:
./switch_roce_link.sh port0
port0 表示保留线 0。脚本会把两端切换为:
NCCL_IB_HCA=rocep1s0f0,roceP2p1s0f0
NCCL_SOCKET_IFNAME=enp1s0f0np0,enP2p1s0f0np0
CONTROL_IF=enp1s0f0np0
MASTER_ADDR=10.10.1.25
HEAD_ROCE_IP=10.10.1.25
WORKER_ROCE_IP=10.10.1.27
WORKER_SSH_TARGET=root@10.10.1.27
4. 线 0 故障,切换到线 1
./switch_roce_link.sh port1
port1 表示保留线 1,对应配置为:
NCCL_IB_HCA=rocep1s0f1,roceP2p1s0f1
NCCL_SOCKET_IFNAME=enp1s0f1np1,enP2p1s0f1np1
CONTROL_IF=enp1s0f1np1
MASTER_ADDR=10.10.2.25
HEAD_ROCE_IP=10.10.2.25
WORKER_ROCE_IP=10.10.2.27
WORKER_SSH_TARGET=root@10.10.2.27
记住一个对应关系就够了:参数指定的是准备使用的健康线。线 0 故障选 port1,线 1 故障选 port0。
5. 线缆修复后,恢复双线
先按第六节确认四组网络路径恢复,再执行:
./switch_roce_link.sh dual
脚本的 dual 模式使用以下固定映射:
NCCL_IB_HCA=rocep1s0f0,rocep1s0f1,roceP2p1s0f0,roceP2p1s0f1
NCCL_SOCKET_IFNAME=enp1s0f1np1,enP2p1s0f1np1
CONTROL_IF=enp1s0f1np1
MASTER_ADDR=10.10.1.25
HEAD_ROCE_IP=10.10.2.25
WORKER_ROCE_IP=10.10.2.27
WORKER_SSH_TARGET=root@10.10.2.27
这里四个 HCA 都进入候选集合,socket 和控制接口使用线 1,初始化地址使用线 0。执行 dual 是应用这套脚本配置,并非逐字还原第九节手工配置时的所有网络字段。采用脚本运维后,以这张模式映射为准。
6. 脚本执行了哪些操作
整个切换过程依次完成:
- 获取操作锁,检查前一次配置事务状态。
- 通过目标线检查 Worker 的 SSH、Docker 和两端目标接口 carrier。
- 备份配置,生成目标参数,分阶段同步并提交两端 .env。
- 调用 ./stop.sh 停止双机,再调用 ./start.sh both 重建服务。
- 轮询 Head 的 /health,就绪后发起带凭据的最小问答。
- 检查响应完整性、正常结束及答案,成功时返回退出码 0。
这是运维人员触发、由脚本自动执行的受控恢复。切换包含模型重载,会中断正在处理的请求,服务恢复后再由业务端按规则重试。
7. 保存切换日志和退出码
在 Bash 中执行,例如切换到线 1:
umask 077
mkdir -p logs
set -o pipefail
./switch_roce_link.sh port1 2>&1 | tee "logs/switch-port1-$(date +%Y%m%d-%H%M%S).log"
rc=${PIPESTATUS[0]}
printf 'switch_exit=%s\n' "$rc"
成功输出会包含最小问答验证通过,以及目标模式切换完成。随后再次查看状态,并从客户端发送一个实际请求:
./switch_roce_link.sh status
curl --fail-with-body -sS --max-time 10 \
http://127.0.0.1:8100/health
脚本健康等待采用最多 60 轮检查,每轮间隔 5 秒,另有请求自身的耗时;完整切换还包含停机、创建和模型初始化时间。
如果返回非零,先检查两端日志、容器状态和 .tx_roce_switch.state。配置提交成功与模型服务启动成功是两个阶段;健康或问答检查失败后,当前脚本会报告失败退出,后续恢复由运维人员根据证据处理。
脚本还提供只更新配置的方式:
./switch_roce_link.sh port1 --no-restart
这个参数会同步两端 .env,保留当前运行容器。它适合提前准备配置,实际链路切换仍需完成双机重建;故障恢复时使用前面的普通命令。





