借用「服务器A」的 GPU 资源,运行「服务器B」上的代码与环境(NFSv4 零拷贝方案)
借用「服务器A」的 GPU 资源,运行「服务器B」上的代码与环境(NFSv4 零拷贝方案)
前言:
实验室有两个服务器 我在一个服务器(B)部署好环境和代码 但是这个服务器老是被其他人占用,导致使用不了,于是想到用另外一个空闲服务器,借用空闲服务器(A)的资源跑B的代码,不用复制环境就可以直接访问,以下是GPT给的答案,已成功部署。
服务器A-用户A(拥有空闲 GPU 的机器)
服务器B-用户B(保存代码/数据/conda 环境的机器)
IP 示例:A_IP、B_IP
家目录示例:/home/userB
目标:在服务器A上使用服务器B的代码、数据与 Conda 环境进行长期训练/调试——只借用 A 的算力,不复制代码与环境。
核心方法:NFSv4 将 B 的目录导出到 A,并在 A 上直接调用 B 的环境解释器运行。
总览
- 在 服务器B:安装 NFS 服务端并导出
/home/userB(包含代码、数据与 Conda)。 - 在 服务器A:安装 NFS 客户端,挂载 B 的
/home/userB到/mnt/homeB。 - (可选)在 A 上做 bind mount,让写死的绝对路径无改动可用。
- 在 服务器A(用户A):直接使用 B 的环境解释器(
/mnt/homeB/.conda/envs/ENV_NAME/bin/python)运行训练脚本。 - 可选进阶:使用
conda-pack将环境一次打包到 A(若你不想依赖远端 conda / 兼容性问题)。
前置条件
- 两台服务器处于同一网络、可互通(A 能访问 B 的 NFS 相关端口,默认 TCP/UDP 2049 等)。
- A 和 B 都是 Linux(示例以 Ubuntu 22.04 为参考)。
- 你在两台机上都有 root/sudo 权限用于一次性安装/配置 NFS;训练本身用普通用户A 完成。
步骤一:在服务器B配置 NFS 服务端(以 root/sudo 执行)
1. 安装并启用 NFS
sudo apt-get update
sudo apt-get install -y nfs-kernel-server
2. 确认用户B的 UID/GID(后续用于 all_squash 映射)
id -u userB # 记下 UID,比如 1006
id -g userB # 记下 GID,比如 1006
3. 导出 整个家目录 /home/userB(包含代码/数据/conda)
只允许 服务器A 访问;将所有写入映射为
userB身份(all_squash + anonuid/anongid),避免权限错乱。
# 备份
sudo cp /etc/exports /etc/exports.bak.$(date +%F-%H%M%S)
# 追加导出规则(把 1006/1006 替换为 userB 的真实 UID/GID;把 A_IP 替换为服务器A的IP)
echo "/home/userB A_IP(rw,sync,no_subtree_check,all_squash,anonuid=1006,anongid=1006)" | sudo tee -a /etc/exports
# 让导出表生效
sudo exportfs -ra
sudo exportfs -v
若
nfs-server启动失败并报类似“Cannot allocate memory”可适当降低线程数:
# 降低 nfsd 线程数(例如 2)
echo 'RPCNFSDCOUNT=2' | sudo tee -a /etc/default/nfs-kernel-server
sudo systemctl daemon-reload
sudo systemctl restart nfs-server
sudo systemctl status nfs-server --no-pager -l
步骤二:在服务器A配置 NFS 客户端(以 root/sudo 执行)
1. 安装客户端
sudo apt-get update
sudo apt-get install -y nfs-common
2. 挂载 B 的家目录到 A 的 /mnt/homeB
sudo mkdir -p /mnt/homeB
# 写入 fstab(NFSv4,自动挂载 + 断线重连;根据内核情况可去掉 nconnect)
LINE="B_IP:/home/userB /mnt/homeB nfs4 defaults,_netdev,x-systemd.automount,noatime,nconnect=4,rsize=1048576,wsize=1048576 0 0"
grep -qF -- "$LINE" /etc/fstab || echo "$LINE" | sudo tee -a /etc/fstab
sudo systemctl daemon-reload
sudo mount -a
mount | grep /mnt/homeB || systemctl status mnt-homeB.automount
3. 验证读写映射
# 普通用户A即可写入;在B上应看到属主属组均为 userB
echo "hello via NFS" > /mnt/homeB/_nfs_test.txt
ls -l /mnt/homeB/_nfs_test.txt
步骤三(可选但强烈推荐):bind 到原始绝对路径
很多项目把路径写死为 /home/userB/...。为了不改代码,在 A 上做一个 bind mount:
# 以 root/sudo 在A上执行
sudo mkdir -p /home/userB
sudo mount --bind /mnt/homeB /home/userB
# (可选)开机自动 bind
LINE="/mnt/homeB /home/userB none bind 0 0"
grep -qF -- "$LINE" /etc/fstab || echo "$LINE" | sudo tee -a /etc/fstab
完成后,A 上访问 /home/userB 与访问 /mnt/homeB 等价,所有写死路径都能直接工作。
步骤四:在服务器A(用户A)直接调用 B 的 Conda 环境解释器运行
不一定要
conda activate。直接调用环境里的 python 更稳:
例如 B 的环境名为ENV_NAME,其解释器路径通常是:
/mnt/homeB/.conda/envs/ENV_NAME/bin/python
或(如果 B 用的是系统安装的 conda base):/mnt/homeB/miniconda3/envs/ENV_NAME/bin/python
1. 找到环境与项目
# 查看是否存在期望的环境与工程(示例)
ls -d /mnt/homeB/.conda/envs/ENV_NAME
ls -d /mnt/homeB/Experiment/YourProject/YourRepo
2. 单卡训练(示例)
cd /mnt/homeB/Experiment/YourProject/YourRepo
# 选择一张空闲物理卡(例如物理 #5;先用 nvidia-smi 查)
export CUDA_VISIBLE_DEVICES=5
# 显存碎片策略(稳妥方案;你也可以用 64 或 256)
export PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128"
# 直接用环境 python 启动(无需 activate)
/mnt/homeB/.conda/envs/ENV_NAME/bin/python -u main.py
--project_name YourProjectName
--data_name YourDatasetName
--dataset YourDatasetClass
--file_root /home/userB/Experiment/datasets/YourDatasetRoot
--img_size 256
--batch_size 8
--max_epochs 100
--net_G STR
--loss ce
--optimizer sgd
--lr 0.0001
--gpu_ids 0 # 逻辑 GPU 索引(只暴露1张卡时它就是0)
说明:
CUDA_VISIBLE_DEVICES=5表示只暴露 A 的物理 #5 给当前进程,程序内它是逻辑 0。- 很多代码用
--gpu_ids传逻辑编号;若你的 argparse 不支持这个参数,就删掉--gpu_ids 0。- 如果代码里写死了
/home/userB/...,由于我们在 A 做了 bind,依然能读到。
3. 多卡训练(示例,DataParallel 风格)
例如 A 上物理 2、3、5、7 空闲:
# 暴露四张卡给进程,程序内变成逻辑 0,1,2,3
export CUDA_VISIBLE_DEVICES=2,3,5,7
export PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128"
cd /mnt/homeB/Experiment/YourProject/YourRepo
/mnt/homeB/.conda/envs/ENV_NAME/bin/python -u main.py
...
--batch_size 8 # 先保持;稳定后可逐步增大
--lr 0.0001
--gpu_ids 0,1,2,3
如果你的项目支持 DDP(
torchrun --nproc_per_node=4),优先用 DDP,扩展性更好。否则上述DataParallel也可。
步骤五(可选):没有 base conda?用 conda-pack 一次打包到 A
如果 B 的 conda base 不在 /home/userB(例如在 /opt/miniconda3),不方便另外导出,也可以将 B 的环境打包到 A 本地:
在 B(用户B):
conda install -y -n ENV_NAME conda-pack
conda activate ENV_NAME
conda pack -n ENV_NAME -o ~/ENV_NAME_env.tar.gz
把包传到 A 并展开(用户A):
scp userB@B_IP:~/ENV_NAME_env.tar.gz ~/
mkdir -p ~/conda_envs/ENV_NAME
tar -xzf ~/ENV_NAME_env.tar.gz -C ~/conda_envs/ENV_NAME
~/conda_envs/ENV_NAME/bin/conda-unpack
# 之后用本地路径启动(更独立、免远端波动)
~/conda_envs/ENV_NAME/bin/python -u main.py ...
后台运行与会话管理
tmux(推荐)
# 安装(一次性,需 sudo):sudo apt-get install -y tmux
tmux new -s train
# 会话里执行你的启动命令;按 Ctrl+B 然后 D 可后台
tmux attach -t train # 回来
tmux ls # 列表
tmux kill-session -t train
nohup(不需要安装)
mkdir -p logs
nohup /mnt/homeB/.conda/envs/ENV_NAME/bin/python -u main.py ...
> logs/train_$(date +%F_%H%M%S).log 2>&1 &
tail -f logs/train_*.log
常见问题排查
1) FileNotFoundError: /home/userB/... not found
-
说明代码写死了绝对路径,而 A 未做 bind。
解决:在 A 上执行sudo mkdir -p /home/userB sudo mount --bind /mnt/homeB /home/userB然后把启动命令里的路径改回
/home/userB/...。
2) CUDA out of memory 但显存没满(碎片)
-
使用稳妥的分配器参数:
export PYTORCH_CUDA_ALLOC_CONF="max_split_size_mb:128" -
或降低
--batch_size,开启 AMP / gradient checkpoint。
3) 见到 expandable_segments 相关崩溃(部分 PyTorch/驱动组合)
-
不要用
expandable_segments:True;改用上面稳妥配置或:export PYTORCH_CUDA_ALLOC_CONF="backend:cudaMallocAsync,max_split_size_mb:128"
4) --gpu_ids 的理解
- 先用
CUDA_VISIBLE_DEVICES=物理列表选择要用的卡; - 程序里总是用逻辑编号(0 开始)。例如
CUDA_VISIBLE_DEVICES=5则程序内只有逻辑 0;CUDA_VISIBLE_DEVICES=2,3,5,7则逻辑0,1,2,3。
5) NFS 服务启动失败或资源不足
- 检查
/etc/exports语法、权限; - 降低 nfsd 线程数(
RPCNFSDCOUNT=2); sudo systemctl restart nfs-server && sudo journalctl -u nfs-server -b -n 200查看日志。
安全建议
-
/etc/exports中只允许A_IP访问;保留root_squash与all_squash,并映射到userB的UID/GID。 -
若需要临时禁止访问:
- 在 B:
sudo exportfs -u A_IP:/home/userB - 在 A:
sudo umount /mnt/homeB
- 在 B:
小结
- NFSv4 + bind mount 能做到不改代码、不复制环境,在服务器A上直接使用服务器B的代码/数据/环境长期训练。
- 训练阶段不需要 sudo;sudo 只用于首次安装与挂载配置。
- 如果遇到兼容性/稳定性问题,
conda-pack是一条非常稳健的后备方案。
需要把以上命令生成成你环境的“一键脚本”(替换具体
A_IP/B_IP/ENV_NAME),可以告诉我你的变量名,我直接给你可粘贴版。







