适用场景:两台主机,每台 8×V100;两台机器共享
/mnt;HostA 上已有可正常运行的 Docker 环境,希望迁移到 HostB,并使用 16 张 GPU 进行多机分布式运行,同时验证 NCCL 是否通过 InfiniBand 通信。
1. 当前环境信息
HostA
text
IP: 192.168.206.140
GPU: 8 × V100
容器名: SD_jgy
原始镜像: sd_jgy:dt12_6
HostA 容器内已经看到:
text
/sys/class/infiniband:
mlx5_0
mlx5_1
并且:
text
mlx5_0: ACTIVE, InfiniBand
mlx5_1: ACTIVE, InfiniBand
需要进一步确认宿主机与容器看到的 HCA 数量是否一致,以及两个 HCA 的速率。
HostB
text
IP: 192.168.205.32
GPU: 8 × V100
HostB 容器内已经看到:
text
/sys/class/infiniband:
mlx5_0
mlx5_1
mlx5_2
mlx5_3
实际状态:
text
mlx5_0: DOWN, InfiniBand, 10 Gb/sec (4X SDR)
mlx5_1: DOWN, InfiniBand, 10 Gb/sec (4X SDR)
mlx5_2: ACTIVE, InfiniBand, 100 Gb/sec (4X EDR)
mlx5_3: DOWN, InfiniBand, 10 Gb/sec (4X SDR)
因此 HostB 当前应优先使用:
bash
export NCCL_IB_HCA=mlx5_2
而不是把四个 HCA 全部交给 NCCL。
2. 整体思路
双机 16 卡并不是"一台 Docker 直接访问另一台机器的 GPU",而是:
text
HostA:
Docker 容器
GPU rank 0~7
HostB:
Docker 容器
GPU rank 8~15
两个容器:
使用相同代码
使用兼容的 Python/CUDA/NCCL 环境
通过 torch.distributed / NCCL 建立 world_size=16 的进程组
推荐流程:
text
HostA 清理现有容器
↓
docker commit 生成新镜像
↓
docker save + zstd 写入共享 /mnt
↓
HostB docker load
↓
HostA / HostB 启动容器并暴露 GPU + IB
↓
检查 GPU / HCA / verbs
↓
检查两节点 TCP bootstrap 网络
↓
16 进程 torchrun + NCCL all_reduce
↓
查看 NCCL 日志是否出现 NET/IB
3. Docker 容器空间清理
3.1 查看容器大小
宿主机执行:
bash
docker ps -s
最初 HostA 的结果:
text
SIZE: 147GB
virtual: 218GB
清理之后:
text
SIZE: 45.1GB
virtual: 116GB
说明容器可写层从 147GB 降到了 45.1GB。
3.2 查看容器内部根目录占用
进入容器:
bash
docker exec -it SD_jgy bash
检查:
bash
du -xhd1 / 2>/dev/null | sort -h
最初发现:
text
/root 127G
/usr 23G
/opt 5.2G
因此继续排查:
bash
du -xhd1 /root 2>/dev/null | sort -h
发现:
text
/root/.cache 80G
/root/anaconda 30G
/root/.vscode-server 7.2G
/root/.npm 4.9G
/root/.lingma 1.5G
3.3 继续定位 .cache
bash
du -xhd1 /root/.cache 2>/dev/null | sort -h
主要占用:
text
/root/.cache/huggingface 66G
/root/.cache/uv 14G
由于模型权重已经确认不再需要,因此删除:
bash
rm -rf /root/.cache
mkdir -p /root/.cache
删除后:
text
/root: 127G → 47G
3.4 可清理的远程开发与 npm 缓存
在确认不需要这些远程编辑器服务状态后,可删除:
bash
rm -rf /root/.vscode-server
rm -rf /root/.cursor-server
rm -rf /root/.cursor
rm -rf /root/.lingma
rm -rf /root/.npm
Codex 注意事项
通过以下命令确认 Codex 安装位置:
bash
which codex
which -a codex
npm root -g
npm ls -g @openai/codex --depth=0
本次环境输出:
text
/usr/local/nvm/versions/node/v18.20.8/bin/codex
/usr/local/nvm/versions/node/v18.20.8/lib/node_modules
@openai/codex@0.130.0
因此:
text
/root/.npm
只是 npm 缓存,可以删除。
不要误删:
text
/usr/local/nvm/versions/node/v18.20.8/
/root/.codex
3.5 不要误删 Conda 主环境
当前 shell 中有:
text
(parrot)
应先检查:
bash
which python
conda info --envs
如果 parrot 位于:
text
/root/anaconda/envs/parrot
那么:
text
/root/anaconda
不能删除。
可以使用:
bash
conda clean -a -y
清理包缓存,但不要直接删除整个 Conda 安装目录。
3.6 清理后的目录状态
最终容器中:
bash
du -xhd1 / 2>/dev/null | sort -h
结果约为:
text
/root 41G
/usr 23G
/opt 5.2G
/ 71G
此时不建议继续盲删 /usr、/opt 或 /root/anaconda。
4. docker commit、save、load 的关系
关系如下:
text
运行中的容器
│
│ docker commit
▼
Docker 镜像
│
│ docker save
▼
tar / tar.zst 文件
│
│ docker load
▼
另一台机器上的 Docker 镜像
│
│ docker run
▼
新的运行容器
4.1 将容器保存为镜像
HostA:
bash
docker commit SD_jgy sd_jgy:clean
作用:
text
当前运行容器状态 → 新镜像 sd_jgy:clean
docker commit 本身没有百分比进度条。
可以在另一个终端观察:
bash
watch -n 2 "docker images | grep sd_jgy"
或:
bash
sudo iotop -oPa
4.2 压缩镜像到共享目录
创建目录:
bash
mkdir -p /mnt/guoyong/image
执行:
bash
docker save sd_jgy:clean \
| zstd -T0 -3 \
-o /mnt/guoyong/image/sd_jgy_clean.tar.zst
本次镜像原始数据约:
text
117.6 GB
压缩结果约:
text
51 GB
查看文件:
bash
ls -lh /mnt/guoyong/image/sd_jgy_clean.tar.zst
检查压缩文件:
bash
zstd -t /mnt/guoyong/image/sd_jgy_clean.tar.zst
4.3 为什么 docker save | zstd 很慢
该命令实际上同时进行:
text
/var/lib/docker 读取约 116GB 镜像层
↓
docker save 顺序输出 tar stream
↓
zstd 多线程压缩
↓
持续写入共享 /mnt
瓶颈可能是:
text
Docker 本地磁盘读取
zstd CPU 压缩
共享盘 /mnt 的写入带宽
查看文件增长:
bash
watch -n 2 \
"ls -lh /mnt/guoyong/image/sd_jgy_clean.tar.zst"
如果需要更快,可以将:
bash
zstd -T0 -3
改为:
bash
zstd -T0 -1
5. HostB 导入镜像与磁盘空间问题
HostB 导入:
bash
zstd -dc /mnt/guoyong/image/sd_jgy_clean.tar.zst \
| sudo docker load
注意不要写成:
bash
sudo zstd -dc xxx | docker load
因为 sudo 只作用在管道左侧。
5.1 no space left on device
本次曾出现:
text
Error processing tar file:
no space left on device
原因:
text
51GB 是压缩文件大小
docker load 后需要把镜像层写入 Docker Root Dir
原始镜像约 116GB
查看 Docker Root Dir:
bash
docker info | grep "Docker Root Dir"
查看其所在分区空间:
bash
df -h /var/lib/docker
注意:
bash
df -h /var/lib/docker
显示的是 /var/lib/docker 所在整个文件系统的容量,不是 Docker 目录自身大小。
例如:
text
Size: 877G
Used: 508G
Avail: 333G
Mounted on: /
意思是:
text
根分区 / 总容量 877G
整个根分区已使用 508G
还剩 333G
5.2 查看 Docker 自身实际占用
bash
sudo du -h --max-depth=1 /var/lib/docker \
2>/dev/null | sort -h
本次结果:
text
75G /var/lib/docker
75G /var/lib/docker/overlay2
Docker 统计:
bash
sudo docker system df
曾显示:
text
Images 28.29GB
Containers 10.04GB
du 与 docker system df 不完全一致是正常的,因为 overlay2、层存储与逻辑镜像统计方式不同。
5.3 删除 HostB 无用停止容器
本次删除了多个 Exited 容器:
bash
sudo docker rm -f \
0c32d3900cc7 \
ea299c60a5bd \
876eb50eddcf \
fa0c2a8ae631 \
6999d3521863
清理后根分区:
text
Used: 798G → 508G
Avail: 43G → 333G
释放约 290GB。
进一步可检查:
bash
sudo docker image prune -af
sudo docker builder prune -af
执行前应确认没有需要保留的镜像。
6. HostA / HostB Docker 启动方式
推荐两台机器启动同一镜像。
例如:
bash
sudo docker run -dit \
--name sd_jgy_16gpu \
--gpus all \
--runtime=nvidia \
--net=host \
--ipc=host \
--shm-size=64g \
--ulimit memlock=-1 \
--ulimit stack=67108864 \
--privileged \
-v /dev/infiniband:/dev/infiniband \
-v /mnt:/mnt \
-v /mnt/guoyong/SD_DockerShared:/workspace \
-w /workspace \
sd_jgy:clean \
/sbin/init
进入:
bash
sudo docker exec -it sd_jgy_16gpu bash
6.1 --net host 与 -p 的关系
旧命令类似:
bash
-p 50022:22
--net host
使用:
bash
--net=host
后,容器共享宿主机网络栈,Docker 端口映射通常没有必要,因此:
bash
-p 50022:22
可以删除。
7. 判断容器是否看到 IB
7.1 检查 RDMA 设备节点
bash
ls /dev/infiniband
例如 HostB:
text
issm0
issm1
issm2
issm3
rdma_cm
umad0
umad1
umad2
umad3
uverbs0
uverbs1
uverbs2
uverbs3
如果 /dev/infiniband 不存在,容器通常无法使用 RDMA verbs。
7.2 检查 HCA
bash
ls /sys/class/infiniband
HostA:
text
mlx5_0
mlx5_1
HostB:
text
mlx5_0
mlx5_1
mlx5_2
mlx5_3
7.3 检查状态、链路类型和速率
bash
for hca in /sys/class/infiniband/*; do
echo "==== $(basename $hca) ===="
for p in $hca/ports/*; do
echo -n "state: "
cat $p/state
echo -n "phys_state: "
cat $p/phys_state 2>/dev/null
echo -n "link_layer: "
cat $p/link_layer
echo -n "rate: "
cat $p/rate
done
done
判断标准:
text
state: 4: ACTIVE
link_layer: InfiniBand
才表示链路处于可用状态。
8. 如何判断 HostA HCA 数量差异是宿主机还是 Docker 问题
HostA 容器里目前只看到:
text
mlx5_0
mlx5_1
HostB 容器里看到四个:
text
mlx5_0
mlx5_1
mlx5_2
mlx5_3
不能直接判断是 Docker 问题。
需要宿主机和容器对比。
8.1 HostA 宿主机检查
退出容器,在 HostA 宿主机执行:
bash
ls /sys/class/infiniband
ls /dev/infiniband
再检查:
bash
for hca in /sys/class/infiniband/*; do
echo "==== $(basename $hca) ===="
for p in $hca/ports/*; do
echo -n "state: "
cat $p/state
echo -n "link_layer: "
cat $p/link_layer
echo -n "rate: "
cat $p/rate
done
done
查看 PCIe 设备:
bash
lspci | grep -Ei "Mellanox|ConnectX|InfiniBand|MT"
判断:
text
宿主机 2 个,容器 2 个
→ HostA 本身只暴露 2 个 HCA,Docker 没问题
宿主机 4 个,容器 2 个
→ Docker 设备暴露不完整
宿主机 4 个,但只有部分 ACTIVE
→ 只能使用 ACTIVE 的 HCA
8.2 查看容器启动参数
bash
sudo docker inspect <container_name> \
--format 'Privileged={{.HostConfig.Privileged}} NetworkMode={{.HostConfig.NetworkMode}}'
查看设备:
bash
sudo docker inspect <container_name> \
--format '{{json .HostConfig.Devices}}'
9. 查看 GPU 与 IB HCA 拓扑关系
最重要命令:
bash
nvidia-smi topo -m
重点看:
text
GPU0~GPU7
mlx5_0
mlx5_1
...
拓扑距离常见含义:
text
PIX 同一 PCIe Switch
PXB 跨多个 PCIe Bridge
PHB 跨 Host Bridge
NODE 同一 NUMA node
SYS 跨 CPU socket / UPI/QPI
一般距离大致:
text
PIX < PXB < PHB < NODE < SYS
查看 GPU PCI 地址:
bash
nvidia-smi \
--query-gpu=index,name,pci.bus_id \
--format=csv
查看 HCA PCI 路径:
bash
for hca in /sys/class/infiniband/*; do
echo "$(basename $hca)"
readlink -f $hca/device
echo -n "numa_node="
cat $hca/device/numa_node
done
如果系统有:
bash
ibdev2netdev
执行:
bash
ibdev2netdev
可以查看:
text
mlx5_X port Y → Linux netdev
10. HostA 单节点 IB 健康检查
HostA 单独可以验证:
text
IB HCA 存在
verbs 设备存在
端口 ACTIVE
链路类型是 InfiniBand
驱动和 sysfs 正常
但单机不能证明:
text
HostA ↔ HostB 的 IB 网络一定互通
检查:
bash
ls /dev/infiniband
ls /sys/class/infiniband
然后:
bash
for hca in /sys/class/infiniband/*; do
echo "==== $(basename $hca) ===="
for p in $hca/ports/*; do
cat $p/state
cat $p/phys_state 2>/dev/null
cat $p/link_layer
cat $p/rate
cat $p/lid 2>/dev/null
done
done
如果安装了工具:
bash
ibv_devinfo
rdma link
11. 两节点 TCP bootstrap 互通测试
HostA:
text
192.168.206.140
HostB:
text
192.168.205.32
两者不在同一个 /24 网段,但本次已经验证 HostB 可以 ping HostA:
bash
ping -c 3 192.168.206.140
结果:
text
0% packet loss
RTT 约 0.2~0.35 ms
这只证明:
text
TCP/IP 路由可达
不证明 NCCL 数据路径一定使用 IB。
11.1 端口连通测试
HostA:
bash
python -m socketserver 29500
HostB:
bash
python - <<'PY'
import socket
s = socket.create_connection(
("192.168.206.140", 29500),
timeout=5,
)
print("connect ok")
s.close()
PY
12. 16 卡 torchrun + NCCL all_reduce 测试
创建共享脚本:
bash
cat > /mnt/guoyong/test_dist_16gpu.py <<'PY'
import os
import socket
import torch
import torch.distributed as dist
dist.init_process_group(backend="nccl")
rank = dist.get_rank()
world_size = dist.get_world_size()
local_rank = int(os.environ["LOCAL_RANK"])
torch.cuda.set_device(local_rank)
# correctness test
x = torch.tensor(
[rank],
dtype=torch.float32,
device="cuda",
)
dist.all_reduce(
x,
op=dist.ReduceOp.SUM,
)
# larger communication tensor
y = torch.ones(
256 * 1024 * 1024 // 4,
dtype=torch.float32,
device="cuda",
)
y *= rank
dist.all_reduce(
y,
op=dist.ReduceOp.SUM,
)
torch.cuda.synchronize()
print(
f"host={socket.gethostname()} "
f"rank={rank} "
f"local_rank={local_rank} "
f"world_size={world_size} "
f"sum_rank={x.item()} "
f"y0={y[0].item()}"
)
dist.barrier()
dist.destroy_process_group()
PY
正确结果:
text
world_size=16
sum_rank=120
y0=120.0
因为:
text
0 + 1 + ... + 15 = 120
13. HostA torchrun 启动模板
HostA:
bash
export MASTER_ADDR=192.168.206.140
export MASTER_PORT=29500
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=INIT,NET
export NCCL_IB_DISABLE=0
# 根据 HostA 实际 ACTIVE HCA 调整
export NCCL_IB_HCA=mlx5_0,mlx5_1
# 填写 192.168.206.140 对应的 Linux 网络接口
export NCCL_SOCKET_IFNAME=enp1s0f0
torchrun \
--nnodes=2 \
--nproc_per_node=8 \
--node_rank=0 \
--master_addr=$MASTER_ADDR \
--master_port=$MASTER_PORT \
/mnt/guoyong/test_dist_16gpu.py \
2>&1 | tee /tmp/nccl_hostA.log
14. HostB torchrun 启动模板
HostB 先确认:
bash
ip -br addr
找到:
text
192.168.205.32
对应的接口名。
然后:
bash
export MASTER_ADDR=192.168.206.140
export MASTER_PORT=29500
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=INIT,NET
export NCCL_IB_DISABLE=0
# HostB 当前只有 mlx5_2 ACTIVE
export NCCL_IB_HCA=mlx5_2
# 替换为 HostB 上 192.168.205.32 对应的接口
export NCCL_SOCKET_IFNAME=<HOSTB_NETDEV>
torchrun \
--nnodes=2 \
--nproc_per_node=8 \
--node_rank=1 \
--master_addr=$MASTER_ADDR \
--master_port=$MASTER_PORT \
/mnt/guoyong/test_dist_16gpu.py \
2>&1 | tee /tmp/nccl_hostB.log
启动顺序建议:
text
先启动 HostA
再启动 HostB
HostA 会等待 HostB 加入 rendezvous。
15. 如何确认 NCCL 真的走 IB
测试完成后:
HostA:
bash
grep -E \
"NET/IB|NET/Socket|mlx5|Using network|Bootstrap" \
/tmp/nccl_hostA.log
HostB:
bash
grep -E \
"NET/IB|NET/Socket|mlx5|Using network|Bootstrap" \
/tmp/nccl_hostB.log
判断:
text
看到 NET/IB
看到具体 mlx5_X
→ NCCL 数据路径使用 RDMA/InfiniBand
只看到 NET/Socket
→ NCCL 通过 TCP Socket 通信
注意:
text
ping 通 ≠ NCCL 使用 IB
真正应以 NCCL 日志为准。
16. 当前最合理的 NCCL HCA 配置
根据目前已知状态:
HostA
当前容器看到:
text
mlx5_0 ACTIVE InfiniBand
mlx5_1 ACTIVE InfiniBand
可先使用:
bash
export NCCL_IB_HCA=mlx5_0,mlx5_1
但应先在宿主机确认 HCA 数量、状态和速率是否与容器一致。
HostB
只有:
text
mlx5_2 ACTIVE 100 Gb/sec EDR
因此使用:
bash
export NCCL_IB_HCA=mlx5_2
17. 常见误区
误区 1:ip addr 没有 ib0 就说明不能用 IB
不一定。
真正 RDMA 数据通信可以通过:
text
mlx5_X verbs device
完成。
是否有 ib0 主要涉及 IPoIB 接口。
误区 2:NCCL_SOCKET_IFNAME 决定 IB 数据链路
不完全正确。
例如:
bash
export NCCL_SOCKET_IFNAME=enp1s0f0
export NCCL_IB_HCA=mlx5_2
可以理解为:
text
enp1s0f0
→ bootstrap / socket 初始化网络
mlx5_2
→ NCCL RDMA 数据通道
最终仍要看 NCCL 日志。
误区 3:HostA 和 HostB 的 HCA 名必须一致
不要求。
HostA 可以:
bash
export NCCL_IB_HCA=mlx5_0,mlx5_1
HostB 可以:
bash
export NCCL_IB_HCA=mlx5_2
NCCL_IB_HCA 是本地设备选择。
误区 4:Docker 里看到 HCA 就一定能通信
不一定。
必须同时满足:
text
/dev/infiniband 可见
/sys/class/infiniband 可见
端口 ACTIVE
link_layer 为 InfiniBand
两节点 Fabric 互通
NCCL verbs 后端可用
18. 推荐后续验证顺序
严格按以下顺序:
text
1. HostA 宿主机检查 HCA 数量、ACTIVE 状态、速率
2. HostA 容器与宿主机对比
3. HostB 容器确认 mlx5_2 ACTIVE 100Gb/s
4. 两边确认 8 张 GPU
5. 两边确认 /dev/infiniband
6. 检查 HostA:29500 端口互通
7. 运行 world_size=16 的 all_reduce
8. 检查 sum_rank=120
9. 检查 NCCL 日志出现 NET/IB
10. 再运行正式 HunyuanDiT / xDiT / DiT 推理程序
19. 一组最小排障命令
GPU
bash
nvidia-smi -L
nvidia-smi topo -m
网络接口
bash
ip -br addr
ip route
IB HCA
bash
ls /sys/class/infiniband
ls /dev/infiniband
IB 状态
bash
for hca in /sys/class/infiniband/*; do
echo "==== $(basename $hca) ===="
for p in $hca/ports/*; do
cat $p/state
cat $p/link_layer
cat $p/rate
done
done
RDMA 工具
bash
ibv_devinfo
rdma link
ibdev2netdev
Docker
bash
docker ps -s
docker system df
docker info | grep "Docker Root Dir"
sudo du -h --max-depth=1 /var/lib/docker | sort -h
NCCL
bash
export NCCL_DEBUG=INFO
export NCCL_DEBUG_SUBSYS=INIT,NET
日志判断:
bash
grep -E \
"NET/IB|NET/Socket|mlx5|Using network|Bootstrap" \
/tmp/nccl_hostA.log
20. 最终建议
对于以后换机器,建议将整个环境拆成四层管理:
text
Docker 镜像
→ 只保存系统库、CUDA/NCCL、Conda/Python 环境
/mnt/models
→ 模型权重
/mnt/workspace
→ 项目代码
/mnt/results
→ 实验结果
避免再把以下内容长期堆积到容器 writable layer:
text
/root/.cache/huggingface
/root/.cache/uv
/root/.npm
/root/.vscode-server
/root/.cursor-server
模型权重
数据集
大日志
实验输出
这样后续 Docker 镜像会更容易迁移,也更利于双机或多机部署。