完整步骤!如何使用LXD构建多人使用GPU服务器 2022-07-14 | lxd, ubuntu | 知识点 文章目录 格式化硬盘换 apt 源固定内核版本配置宿主机网络安装 lxd、zfs 及 bridge-utils安装宿主机显卡驱动LXD 初始化创建容器更改容器名为容器添加设备和权限制作容器模板更换容器的 apt 源配置容器网络更改容器用户名和密码配置容器 ssh 连接添加初始化容器脚本安装容器的显卡驱动创建容器快照创建容器脚本解决重启宿主机导致容器显卡驱动找不到的问题安装 Anaconda安装 PyTorch设置开机自启动命令 最近深度学习仍然爆火,多人如何优雅地使用 GPU 服务器仍然是值得探讨的问题。 对于单个服务器来说,LXD 仍然是我认为最舒服的多人共用服务器的方式。他避免了权限滥用,能够完整利用硬件,缺点就是显卡驱动需要保持一致,升级较为麻烦。 本教程超级详细,是根据我最近操作录屏总结的最佳步骤,并且为批量操作总结了脚本,保证你能够顺利完成安装。 格式化硬盘 由于 LXD 的容器需要使用 ZFS 文件系统进行管理,因此有两种对硬盘的格式化方式: 所有硬盘作为宿主机系统盘。在 LXD 初始化时创建新的 ZFS 镜像,其占用宿主机硬盘空间; 除了一块硬盘作为宿主机系统盘外,其余硬盘格式化为 ZFS 文件系统,并可以通过 ZFS 构成 raid 阵列。在 LXD 初始化时直接选择已经创建的 ZFS 分区。 第1种方案由于 ZFS 的空间和宿主机共享,可能导致创建的 ZFS Pool 过大导致宿主机硬盘空间过小,届时可能无法通过ssh连接,需要在宿主机手动删除一些文件才可继续使用。 严重警告!!!ZFS 只支持扩容,不支持缩容,否则将带来不可逆的文件丢失。 因此这里推荐使用第2种方式,因为这样宿主机与容器的存储分离开来,存储逻辑更清晰。 宿主机系统推荐使用 Ubuntu Server,即不带桌面的版本,这样作为服务器更稳定。 宿主机安装时硬盘选择了 LVM 分区格式,但是他只分配了 200 GB,其他空间没有利用。因此需要使用以下命令将 LVM 分区扩容,占用整个硬盘空间: sudo fdisk -lsudo pvdisplaysudo vgdisplaysudo lvdisplaysudo lvextend -r -l +100%Free /dev/ubuntu-vg/ubuntu-lvdf -hl 使用下面的命令对 硬盘 /dev/sda 进行 ZFS 的分区格式,如果组 raid 可以上网查。 sudo apt install zfsutils-linux# 创建 ZFS 普通分区sudo zpool create zfs_lvm sda# 创建 ZFS mirror(raid1)分区sudo zpool create zfs_lvm mirror sda sdb# 创建 ZFS raidz(raid5)分区:sudo zpool create zfs_lvm raidz sdb sdc sdd sde 其中 zfs_lvm 为 ZFS Pool 的名字。 换 apt 源 sudo mv /etc/apt/sources.list /etc/apt/sources.list.baksudo vim /etc/apt/sources.list 如下为哈工大源: # 默认注释了源码镜像以提高 apt update 速度,如有需要可自行取消注释deb http://mirrors.hit.edu.cn/ubuntu/ focal main restricted universe multiverse# deb-src http://mirrors.hit.edu.cn/ubuntu/ focal main restricted universe multiversedeb http://mirrors.hit.edu.cn/ubuntu/ focal-updates main restricted universe multiverse# deb-src http://mirrors.hit.edu.cn/ubuntu/ focal-updates main restricted universe multiversedeb http://mirrors.hit.edu.cn/ubuntu/ focal-backports main restricted universe multiverse# deb-src http://mirrors.hit.edu.cn/ubuntu/ focal-backports main restricted universe multiversedeb http://mirrors.hit.edu.cn/ubuntu/ focal-security main restricted universe multiverse# deb-src http://mirrors.hit.edu.cn/ubuntu/ focal-security main restricted universe multiverse# 预发布软件源,不建议启用# deb http://mirrors.hit.edu.cn/ubuntu/ focal-proposed main restricted universe multiverse# deb-src http://mirrors.hit.edu.cn/ubuntu/ focal-proposed main restricted universe multiverse 更新源: sudo apt updatesudo apt upgrade 固定内核版本 sudo apt-mark hold linux-image-generic linux-headers-generic 防止内核升级导致显卡驱动失效。 因为显卡驱动需要编译内核版本,升级内核后显卡驱动需要重新编译安装。由于 LXD 容器共享内核,升级内核会导致所有显卡驱动都需要重新安装。 配置宿主机网络 需要更改宿主机网络为网桥模式,这样才能使容器和宿主机处于同一网络子层,在同一局域网的计算机可以直接 ssh 链接。 Ubuntu 17.10 以后默认使用 Netplan 管理网络。 进入 /etc/netplan/ 目录有一个 yaml 配置文件,下面的命令需要根据自己的 yaml 文件名称自行修改 sudo cp /etc/netplan/01-netcfg.yaml /etc/netplan/01-netcfg.yaml.baksudo vim /etc/netplan/01-netcfg.yaml 如下: # This file describes the network interfaces available on your system# For more information, see netplan(5).network: version: 2 renderer: networkd ethernets: eno1: dhcp4: no dhcp6: no bridges: br0: dhcp4: no dhcp6: no interfaces: - eno1 addresses: [ 192.168.100.123/24 ] gateway4: 192.168.100.254 nameservers: addresses: - 114.114.114.114 - 8.8.8.8 parameters: stp: false forward-delay: 0 addresses: [ 192.168.100.123/24 ] 为任意网络无人占用的 IP 即可。 gateway4: 192.168.100.254 为网关地址。 eno1 为网卡名称,可以使用 ip a 或 ifconfig 命令查看。 应用网络配置: sudo netplan --debug apply 安装 lxd、zfs 及 bridge-utils sudo snap install lxdsudo apt install zfsutils-linux bridge-utils 我们需要安装 LXD 实现虚拟容器,ZFS 作为 LXD 的存储管理工具,bridge-utils 用于搭建网桥。由于 apt 安装的 LXD 不是最新版本,这里使用 snap 安装工具安装 LXD。 安装宿主机显卡驱动 去 NVIDIA 官网 下载最新驱动,这里下载的是 ./NVIDIA-Linux-x86_64-418.56.run。 由于系统是 ubuntu-server,所以简单很多,如果是安装的 ubuntu-desktop,建议用其他电脑 ssh 远程连接后再安装。如果一定要在有 desktop 的系统安装显卡驱动,可以参考:超详细! Ubuntu 18.04 安装 NVIDIA 显卡驱动 安装依赖: sudo apt install gcc g++ make 安装驱动: sudo bash ./NVIDIA-Linux-x86_64-418.56.run 查看显卡: nvidia-smi 此时发现输入 nvidia-smi 命令后需要 3 秒左右才会出结果,并且显卡功率占用较高,没有程序运行就有一百多瓦的功耗。 为了解决这些问题,需要将显卡模式改为持久模式,该命令需要 root 权限: sudo nvidia-smi -pm 1 持久模式使得输出结果反应迅速,并且功耗得到降低。但是重启后该模式会默认关闭,需要添加自启动命令,在后面会讲到。 LXD 初始化 sudo lxd init 在初始化过程中,不要创建新的网桥,已存在的网桥名为 br0,其他设置默认即可。 当采用提前将整个硬盘作为 ZFS 分区,这时在是否创建新的 ZFS Pool 时选 no,并填写已经存在的 ZFS pool 的名字。如下所示: Would you like to use LXD clustering? (yes/no) [default=no]:Do you want to configure a new storage pool? (yes/no) [default=yes]:Name of the new storage pool [default=default]: zfs-poolName of the storage backend to use (lvm, zfs, ceph, btrfs, dir) [default=zfs]:Create a new ZFS pool? (yes/no) [default=yes]: noName of the existing ZFS pool or dataset: zfs_lvmWould you like to connect to a MAAS server? (yes/no) [default=no]:Would you like to create a new local network bridge? (yes/no) [default=yes]: noWould you like to configure LXD to use an existing bridge or host interface? (yes/no) [default=no]: yesName of the existing bridge or host interface: br0Would you like the LXD server to be available over the network? (yes/no) [default=no]:Would you like stale cached images to be updated automatically? (yes/no) [default=yes]Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]: 当采用所有硬盘作为宿主机硬盘时,需要在 LXD 初始化时创建新的 ZFS Pool,ZFS设置大小要尽量大,如下所示: Would you like to use LXD clustering? (yes/no) [default=no]:Do you want to configure a new storage pool? (yes/no) [default=yes]:Name of the new storage pool [default=default]: lxdName of the storage backend to use (btrfs, ceph, dir, lvm, zfs) [default=zfs]:Create a new ZFS pool? (yes/no) [default=yes]:Would you like to use an existing block device? (yes/no) [default=no]:Size in GB of the new loop device (1GB minimum) [default=100GB]: 1200Would you like to connect to a MAAS server? (yes/no) [default=no]:Would you like to create a new local network bridge? (yes/no) [default=yes]: noWould you like to configure LXD to use an existing bridge or host interface? (yes/no) [default=no]: yesName of the existing bridge or host interface: br0Would you like LXD to be available over the network? (yes/no) [default=no]:Would you like stale cached images to be updated automatically? (yes/no) [default=yes]Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]: 创建容器 创建的容器最好和宿主机系统相同。 sudo lxc launch ubuntu:20.04 查看容器列表: sudo lxc list 更改容器名 为了后续方便,我们将容器名进行修改: sudo lxc stop equipped-locustsudo lxc rename equipped-locust templatesudo lxc start template 为容器添加设备和权限 sudo lxc config device add template gpu gpusudo lxc config set template security.nesting truesudo lxc config set template security.privileged true 制作容器模板 先配置一个网络、驱动都正常的容器,制作快照并作为模板,这样需要创建新容器时可以从快照创建,节省时间。 更换容器的 apt 源 与宿主机更换方法相同。 配置容器网络 可以通过容器的 NAME 进入容器: sudo lxc exec template bash 其中 template 为容器名。 进入容器后默认是 root 用户,首先安装 net-tools: apt install net-tools 通过 ifconfig 命令查看网卡名为 eth0: 和宿主机一样,进入 /etc/netplan/ 目录有一个 yaml 配置文件,下面的命令需要根据自己的 yaml 文件名称自行修改: 编辑 yaml 配置文件: mv /etc/netplan/50-cloud-init.yaml /etc/netplan/50-cloud-init.yaml.bakvim /etc/netplan/50-cloud-init.yaml 如下: network: version: 2 ethernets: eth0: dhcp4: no dhcp6: no addresses: - 192.168.100.124/24 gateway4: 192.168.100.254 nameservers: addresses: - 114.114.114.114 - 8.8.8.8 应用网络配置: netplan --debug apply 更改容器用户名和密码 容器默认用户名为 ubuntu,这里想把他改成 tmp,命令如下: usermod -l tmp -d /home/tmp -m ubuntugroupmod -n tmp ubuntu 此时 /home 文件夹下只剩下 tmp 目录。 更改 tmp 用户的密码: passwd tmp 然后输入两次新密码。 配置容器 ssh 连接 apt install openssh-server 编辑 ssh 配置文件: vim /etc/ssh/sshd_config 将 PasswordAuthentication 改为 yes,退出编辑后重启 ssh 服务: systemctl restart sshd 此时可以用 exit 命令退出到宿主机中,尝试用 ssh 命令远程连接容器: ssh tmp@192.168.100.124 输入密码,能登录则没问题。 添加初始化容器脚本 为了方便以后初始化容器,我们将网络初始化等命令写入脚本。 在容器的 /root/ 目录执行 vim init_lxd.sh 命令创建脚本,编辑如下: # !/bin/bashread -p "Enter your last name as the username, such as zhang: " last_nameread -p "Enter the password of the container: " passwordread -p "Enter the IP address:" IPecho "Change username to $last_name"usermod -l $last_name -d /home/$last_name -m tmpgroupmod -n $last_name tmpecho "Change IP to $IP"mv /etc/netplan/50-cloud-init.yaml /etc/netplan/50-cloud-init.yaml.bakecho "network: version: 2 ethernets: eth0: dhcp4: no dhcp6: no addresses: - $IP/24 gateway4: 192.168.100.254 nameservers: addresses: - 114.114.114.114 - 8.8.8.8" > /etc/netplan/50-cloud-init.yamlnetplan --debug applyecho "$last_name:$password" | sudo chpasswdreboot 安装容器的显卡驱动 容器和宿主机的显卡驱动必须保持一致,因此需要将宿主机的驱动文件传输到容器中。 因为刚安好了 ssh,因此可以选择 scp 传输。在宿主机中输入以下命令: scp ./NVIDIA-Linux-x86_64-418.56.run tmp@192.168.100.124:/home/tmp/ 也可以通过 lxc 命令传输。在宿主机中输入以下命令: sudo lxc file push ./NVIDIA-Linux-x86_64-418.56.run template/home/tmp/NVIDIA-Linux-x86_64-418.56.run 以上两种方法均可传输文件。 传输后通过 ssh 进入容器,输入以下命令安装显卡驱动: sudo bash ./NVIDIA-Linux-x86_64-418.56.run --no-kernel-module 由于容器和宿主机共享内核,所以在安装容器的显卡驱动时需要添加 --no-kernel-module 参数。 安装好显卡驱动后用 nvidia-smi 命令查看显卡: 创建容器快照 在宿主机执行以下命令,对 template 容器创建一个名为 gpu 的快照: sudo lxc snapshot template gpu 创建容器脚本 现在容器模板已经制作完成,创建新的容器只需要将容器模板的快照进行复制并恢复,即可得到一个新的容器,但是命令比较复杂,因此我将其整理为脚本。 在宿主机执行 vim create_container.sh 创建脚本文件,编辑如下: # !/bin/bashpasswd='xxxx'read -p "Enter your full name as the container name, such as zhangsan: " nameecho "Create the container $name..."echo $passwd | sudo -S lxc copy template/gpu $nameecho "Start the container $name..."echo $passwd | sudo -S lxc start $namesudo -S lxc exec $name -- /bin/bash 其中 passwd 为容器的默认密码。 解决重启宿主机导致容器显卡驱动找不到的问题 此时如果重启宿主机,我们会发现容器中显卡驱动消失。目前找到的解决办法是在宿主机运行一次 pytorch cuda 的程序,并重启容器。 因此我们需要在宿主机安装 PyTorch,并在开机时自动执行 import torch; print(torch.cuda.is_available()) 命令。 安装 Anaconda 在官网下载 Anaconda3,也可以用以下命令: wget https://repo.anaconda.com/archive/Anaconda3-2022.05-Linux-x86_64.sh 安装Anaconda(一定不要用 sudo): bash ./Anaconda3-2022.05-Linux-x86_64.sh Anaconda 换源(哈工大): 创建 .condarc vim ~/.condarc 编辑如下: channels: - defaultsshow_channel_urls: truedefault_channels: - https://mirrors.hit.edu.cn/anaconda/pkgs/main - https://mirrors.hit.edu.cn/anaconda/pkgs/r - https://mirrors.hit.edu.cn/anaconda/pkgs/msys2custom_channels: conda-forge: https://mirrors.hit.edu.cn/anaconda/cloud msys2: https://mirrors.hit.edu.cn/anaconda/cloud bioconda: https://mirrors.hit.edu.cn/anaconda/cloud menpo: https://mirrors.hit.edu.cn/anaconda/cloud pytorch: https://mirrors.hit.edu.cn/anaconda/cloud pytorch-lts: https://mirrors.hit.edu.cn/anaconda/cloud simpleitk: https://mirrors.hit.edu.cn/anaconda/cloud 运行 conda clean -i 清除索引缓存,保证用的是镜像站提供的索引。 安装 PyTorch 在 PyTorch 官网查看对应版本的安装命令,建议创建一个新的 PyTorch 环境: conda create -n pt1.12 pytorch torchvision torchaudio cudatoolkit=11.6 -c pytorch -c conda-forge 设置开机自启动命令 编辑 rc-local.service: sudo vim /lib/systemd/system/rc-local.service 末尾添加以下三行: [Install] WantedBy=multi-user.target Alias=rc-local.service 新建 rc.local sudo vim /etc/rc.local 编辑如下: #!/bin/shsleep 180ssudo nvidia-smi -pm 1/home/j1812/anaconda3/envs/pt1.12/bin/python -c 'import torch; print(torch.cuda.is_available())'sudo lxc stop template --forcesudo lxc start templateexit 0 增加 rc.local 可执行权限: sudo chmod u+x /etc/rc.local 设置开机启动: sudo systemctl enable rc-localsudo systemctl start rc-local 检查是否启动成功: sudo systemctl status rc-local 至此,已经完成了所有 LXD 的配置。 当不知道哪个容器正在占用显卡时,使用下面的命令查询: nvidia-smi | grep -E 'python.*[0-9]{3,4}MiB' | awk '{print $5}' | xargs -I{} sh -c 'echo "PID: {} Cgroup: $(cat /proc/{}/cgroup | grep rdma | cut -d ":" -f 3)"' 从运行 nvidia-smi 命令结果中获取占用GPU的Python进程的PID,然后通过 xargs 将PID传递给 sh 命令,进而在shell中执行一条命令来查看进程所属的 rdma 类型的cgroup。 具体来说,awk '{print $5}' 的作用是从 nvidia-smi 命令结果中获取占用GPU的Python进程的PID,其中 $5 是因为 nvidia-smi 命令结果中Python进程PID位于第5列。接着,xargs -I{} sh -c 将PID传递给 sh 命令,并在shell中执行一条命令。这条命令通过 cat /proc/{}/cgroup | grep rdma | cut -d ":" -f 3 获取进程所属的 rdma 类型的cgroup路径,并且通过 echo 命令输出PID和对应的cgroup路径。