最近深度学习仍然爆火,多人如何优雅地使用 GPU 服务器仍然是值得探讨的问题。

对于单个服务器来说,LXD 仍然是我认为最舒服的多人共用服务器的方式。他避免了权限滥用,能够完整利用硬件,缺点就是显卡驱动需要保持一致,升级较为麻烦。

本教程超级详细,是根据我最近操作录屏总结的最佳步骤,并且为批量操作总结了脚本,保证你能够顺利完成安装。

格式化硬盘

由于 LXD 的容器需要使用 ZFS 文件系统进行管理,因此有两种对硬盘的格式化方式:

  1. 所有硬盘作为宿主机系统盘。在 LXD 初始化时创建新的 ZFS 镜像,其占用宿主机硬盘空间;
  2. 除了一块硬盘作为宿主机系统盘外,其余硬盘格式化为 ZFS 文件系统,并可以通过 ZFS 构成 raid 阵列。在 LXD 初始化时直接选择已经创建的 ZFS 分区。

第1种方案由于 ZFS 的空间和宿主机共享,可能导致创建的 ZFS Pool 过大导致宿主机硬盘空间过小,届时可能无法通过ssh连接,需要在宿主机手动删除一些文件才可继续使用。

严重警告!!!ZFS 只支持扩容,不支持缩容,否则将带来不可逆的文件丢失。

因此这里推荐使用第2种方式,因为这样宿主机与容器的存储分离开来,存储逻辑更清晰。

宿主机系统推荐使用 Ubuntu Server,即不带桌面的版本,这样作为服务器更稳定。

宿主机安装时硬盘选择了 LVM 分区格式,但是他只分配了 200 GB,其他空间没有利用。因此需要使用以下命令将 LVM 分区扩容,占用整个硬盘空间:

sudo fdisk -l

sudo pvdisplay
sudo vgdisplay
sudo lvdisplay
sudo lvextend -r -l +100%Free /dev/ubuntu-vg/ubuntu-lv

df -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.bak
sudo 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 multiverse
deb 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 multiverse
deb 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 multiverse
deb 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 update
sudo 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.bak
sudo 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 aifconfig 命令查看。

应用网络配置:

sudo netplan --debug apply

安装 lxd、zfs 及 bridge-utils

sudo snap install lxd
sudo 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-pool
Name of the storage backend to use (lvm, zfs, ceph, btrfs, dir) [default=zfs]:
Create a new ZFS pool? (yes/no) [default=yes]: no
Name of the existing ZFS pool or dataset: zfs_lvm
Would 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]: no
Would you like to configure LXD to use an existing bridge or host interface? (yes/no) [default=no]: yes
Name of the existing bridge or host interface: br0
Would 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]: lxd
Name 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]: 1200
Would 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]: no
Would you like to configure LXD to use an existing bridge or host interface? (yes/no) [default=no]: yes
Name of the existing bridge or host interface: br0
Would 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-locust
sudo lxc rename equipped-locust template
sudo lxc start template

为容器添加设备和权限

sudo lxc config device add template gpu gpu
sudo lxc config set template security.nesting true
sudo 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.bak
vim /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 ubuntu
groupmod -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/bash
read -p "Enter your last name as the username, such as zhang: " last_name
read -p "Enter the password of the container: " password
read -p "Enter the IP address:" IP

echo "Change username to $last_name"
usermod -l $last_name -d /home/$last_name -m tmp
groupmod -n $last_name tmp

echo "Change IP to $IP"
mv /etc/netplan/50-cloud-init.yaml /etc/netplan/50-cloud-init.yaml.bak
echo "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.yaml

netplan --debug apply

echo "$last_name:$password" | sudo chpasswd

reboot

安装容器的显卡驱动

容器和宿主机的显卡驱动必须保持一致,因此需要将宿主机的驱动文件传输到容器中。 因为刚安好了 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/bash
passwd='xxxx'

read -p "Enter your full name as the container name, such as zhangsan: " name

echo "Create the container $name..."
echo $passwd | sudo -S lxc copy template/gpu $name

echo "Start the container $name..."
echo $passwd | sudo -S lxc start $name

sudo -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:
- defaults
show_channel_urls: true
default_channels:
- https://mirrors.hit.edu.cn/anaconda/pkgs/main
- https://mirrors.hit.edu.cn/anaconda/pkgs/r
- https://mirrors.hit.edu.cn/anaconda/pkgs/msys2
custom_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/sh
sleep 180s
sudo nvidia-smi -pm 1
/home/j1812/anaconda3/envs/pt1.12/bin/python -c 'import torch; print(torch.cuda.is_available())'
sudo lxc stop template --force
sudo lxc start template
exit 0

增加 rc.local 可执行权限:

sudo chmod u+x /etc/rc.local

设置开机启动:

sudo systemctl enable rc-local
sudo 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路径。