基于LXD的GPU算力虚拟化
[TOC]
搭建需求
由于当前算法和模型对GPU的强烈需求,实验室购置了一台性能强悍的GPU云服务器供大家一起使用。如果所有人对这台服务器拥有控制权是十分危险的,例如误删除他人文件,弄乱他人环境等。最简单的方法是为每位同学配置一台虚拟机,但硬件虚拟化造成大量的资源浪费,同时GPU并不支持常规的虚拟化。
-
云计算资源因安全措施考虑会进行如下设置:
-
设置访问白名单,限制仅实验室环境下访问。外部环境若需要访问计算资源,需先通过VPN接入实验室内网
-
仅开放用于SSH连接的端口到公网
-
基于上述背景整理提出以下需求:
- 独立:不同用户的环境相互独立,可同时使用。
- 隔离:用户不能直接操作宿主机,即用户不能逃逸至宿主机。用户访问宿主机的唯一通道是共享文件夹。
- 自由:用户可以像使用一台自己的Linux机器一样,通过SSH访问,并拥有主机的所有权限。
- GPU:核心需求,每位同学可以直接访问GPU和使用宿主机的所有资源,包括CPU、内存、硬盘等。
- 可控:管理员可以较为方便对每位同学的机器进行管理,如资源争抢严重时,限制每位同学的资源使用上限(GPU, CPU, 内存等)
- 开销: 为满足这些需求,额外的开销应该尽可能小到可以忽略。
- 利用率: 公用算力的资源应该能得到最大化的利用
- 复杂度: 整套解决方案不能太复杂,便于维护
宿主机硬件配置
- GPU NVIDIA Tesla P40 *2
- Memory 64G
- Disk 100G SSD 系统盘 + 500G SSD 数据盘
- CPU Intel Core (Broadwell, no TSX) @ 16x 2.2GHz
- OS Ubuntu 20.04 LTS Server
解决方案
需求中有两个核心点:
- 硬件资源共享
- 用户的隔离
用户隔离
首先解决的问题就是怎么做用户的隔离。最简单的方法无疑是虚拟机,然而存在如下问题:
- 实现显卡虚拟化的成本非常高
- CPU虚拟化的额外开销不低
- IO虚拟化的性能问题
目前很多Hypervisor都支持PCI Passthrough,然而这种技术会使得显卡只能被一台虚拟机独占,其他虚拟机无法使用这块显卡。无法最大化硬件资源的利用率。与之类似的就是CPU和内存资源的划分,使用虚拟化后,一台虚拟机的CPU和内存基本都是定死的,无法按需动态分配。无疑这种静态资源分配策略也降低了硬件资源的利用率。
因此,我们完全不考虑虚拟化。
公用的GPU云服务器是Linux平台,所以我们可以直接利用Linux内核提供的隔离机制来解决这个问题。因为只做隔离,这里带来的额外开销微乎其微。同时硬件资源共享,可以最大化硬件资源资源的利用率。
用来隔离的方法有很多,较为成熟的有:
- OpenVZ: 太过复杂,不考虑
- Docker: 应用级容器
- LXD: 可以看作LXC的升级版,支持跨主机容器迁移和方便的容器管理
- LXC: 起源于cgroup和namespaces,使得进程之间相互隔离,即进程虚拟化
OpenVZ因为太过复杂就不考虑了,LXD和LXC差别不大。因此只比较LXC和Docker即可。
Docker: 更倾向于部署应用与无状态。若把Docker当作虚拟机使用,Docker的接口反而成为一种累赘
LXC: 系统级容器,倾向于提供一台系统级的容器环境
综合比较后选择LXC作为用户隔离方案
硬件资源共享
接下来是另一个重要问题,硬件资源共享。如何在LXC容器中使用GPU是我们非常关心的问题,好在 Linux 有硬件即文件的哲学,我们只要把宿主机中显卡设备对应的文件挂载到 LXC 容器中就能解决这个问题。
整体方案流程
- 宿主机安装深度学习三件套
- 安装Nvidia Driver
- 安装CUDA
- 安装nvidia-container-runtime,便于在容器内可以直接调用宿主机的显卡驱动
- 安装Anaconda,设置共享的conda环境。cudnn 通过conda在不同的虚拟环境下安装
- 安装LXD/ZFS软件进行配置
- 创建容器模板 ( Ubuntu 20.04 ),包括:GPU驱动、共享目录、SSH服务等
- 按需分配,克隆容器模板,并做个性化修改
配置流程
系统更新
# 更新源
$ sudo apt update
# 更新软件
$ sudo apt upgrade
# 安装常用编译工具
$ sudo apt install gcc, g++, make, cmake, build-essential
安装NVIDIA驱动
# 安装ubuntu-drivers 工具
$ sudo apt install ubuntu-drivers-common
使用ubuntu-drivers devices
命令查看显卡型号和推荐驱动版本
ubuntu@10-19-127-48:~$ ubuntu-drivers devices
== /sys/devices/pci0000:00/0000:00:03.0 ==
modalias : pci:v000010DEd00001B38sv000010DEsd000011D9bc03sc02i00
vendor : NVIDIA Corporation
model : GP102GL [Tesla P40]
driver : nvidia-driver-390 - distro non-free
driver : nvidia-driver-440-server - distro non-free
driver : nvidia-driver-455 - distro non-free recommended
driver : nvidia-driver-450 - distro non-free
driver : nvidia-driver-418-server - distro non-free
driver : nvidia-driver-450-server - distro non-free
driver : xserver-xorg-video-nouveau - distro free builtin
卸载原有nvidia驱动并将nouveau驱动加入黑名单并禁用nouveau内核模块
$ sudo apt remove --purge nvidia* -y
# 把 nouveau 驱动加入黑名单并禁用用 nouveau 内核模块
# 在文件 blacklist-nouveau.conf 中加入如下内容
$ sudo echo "blacklist nouveau" >> /etc/modprobe.d/blacklist-nouveau.conf
$ sudo echo "options nouveau modeset=0" >> /etc/modprobe.d/blacklist-nouveau.conf
# 保存退出,执行
$ sudo update-initramfs -u
自动安装推荐驱动(recommended)的驱动:
$ sudo ubuntu-drivers autoinstall
安装成功后,重新登陆系统使用nvidia-smi
命令确认安装成功:
Thu Dec 24 11:31:38 2020
+-----------------------------------------------------------------------------+
| NVIDIA-SMI 455.38 Driver Version: 455.38 CUDA Version: 11.1 |
|-------------------------------+----------------------+----------------------+
| GPU Name Persistence-M| Bus-Id Disp.A | Volatile Uncorr. ECC |
| Fan Temp Perf Pwr:Usage/Cap| Memory-Usage | GPU-Util Compute M. |
| | | MIG M. |
|===============================+======================+======================|
| 0 Tesla P40 Off | 00000000:00:03.0 Off | 0 |
| N/A 25C P8 9W / 250W | 4MiB / 22919MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
| 1 Tesla P40 Off | 00000000:00:04.0 Off | 0 |
| N/A 24C P8 10W / 250W | 4MiB / 22919MiB | 0% Default |
| | | N/A |
+-------------------------------+----------------------+----------------------+
+-----------------------------------------------------------------------------+
| Processes: |
| GPU GI CI PID Type Process name GPU Memory |
| ID ID Usage |
|=============================================================================|
| 0 N/A N/A 2435 G /usr/lib/xorg/Xorg 4MiB |
| 1 N/A N/A 2435 G /usr/lib/xorg/Xorg 4MiB |
+-----------------------------------------------------------------------------+
安装CUDA
- CUDA是NVIDIA推出的用于自家GPUI的并行计算框架
- CUDA只能在NVIDIA的GPU上运行
- 只有当要解决的计算问题是可以大量并行计算的时候才能发挥CUDA的作用
根据nvidia-smi
结果中的信息,我们需要安装cuda 11.1
# 下载runfile
$ wget https://developer.download.nvidia.com/compute/cuda/11.1.1/local_installers/cuda_11.1.1_455.32.00_linux.run
# 安装
$ sudo sh cuda_11.1.1_455.32.00_linux.run
在选择安装内容时,去除驱动选项
配置环境变量,在/etc/profile
中添加
export CUDA_HOME=$CUDA_HOME:/usr/local/cuda-11.1
export PATH=$PATH:$CUDA_HOME/bin
export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/cuda-11.1/lib64
重新载入环境变量
$ source /etc/profile
安装Anaconda3
下载安装Anaconda3
# 下载安装包
$ wget https://mirrors.tuna.tsinghua.edu.cn/anaconda/archive/Anaconda3-2020.11-Linux-x86_64.sh
# 安装
$ sudo sh Anaconda3-2020.11-Linux-x86_64.sh
为实现conda 环境共享,Anaconda 安装目录设置为/opt/anaconda3
更改conda 源为清华源进行加速
# 生成.condarc 文件
$ conda config --set show_channel_urls yes
修改~/.condarc
文件
channels:
- defaults
show_channel_urls: true
# 注释下面一行,防止不同源的包冲突
# channel_alias: https://mirrors.tuna.tsinghua.edu.cn/anaconda
default_channels:
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/main
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/free
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/r
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/pro
- https://mirrors.tuna.tsinghua.edu.cn/anaconda/pkgs/msys2
custom_channels:
conda-forge: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
msys2: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
bioconda: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
menpo: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
pytorch: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
simpleitk: https://mirrors.tuna.tsinghua.edu.cn/anaconda/cloud
更新conda基础环境shell
$ conda update conda --all
$ codna install anaconda
配置Tensorflow2.0环境
# 创建tf2.0虚拟环境
(base) $ conda create -n tf2
# 激活虚拟环境
(base) $ conda activate tf2
# 安装tf2.0 GPU环境
(tf2) $ conda install tensorflow-gpu=2.1
# 回到基础环境便于后续安装操作
(tf2) $ conda deactivate
配置Pytorch 环境
# 创建 Pytorch 虚拟环境
(base) $ conda create -n pytorch
# 激活虚拟环境
(base) $ conda activate pytorch
# 安装 pytorch 虚拟环境。参考 https://pytorch.org/get-started/locally/
(pytorch) $ conda install pytorch torchvision torchaudio cudatoolkit=11.0 -c pytorch
# 回到基础环境便于后续安装操作
(pytorch) $ conda deactivate
配置用于Conda和CUDA 环境共享的用户组
# 新建用户组, 并指定gid, 便于lxd idmap设置
$ sudo groupadd -g 1101 conda_public
# 将当前用户添加到用户组
$ sudo gpasswd -a $(whoami) conda_public
# 修改anaconda 文件夹用户组
$ sudo chown -R $(whoami):conda_public /opt
# 修改anaconda 文件夹权限
$ sudo chmod 775 -R /opt
# 修改cuda 文件夹用户组
$ sudo chown -R $(whoami):conda_public /usr/local/cuda-11.1
# 修改cuda 文件夹权限
$ sudo chmod 775 -R /usr/local/cuda-11.1
容器环境配置
安装lxd、zfs及bridge-utils
$ sudo apt install lxc lxd-client zfsutils-linux bridge-utils
LXD 初始化
$ sudo lxd init
详情如下:
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]:
Name of the storage backend to use (btrfs, 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]:
# 这里我们把默认的存储池设置为 128G,应该足够使用
# 如果根分区的文件系统是 btrfs/zfs,这里会直接跳过,不会有设置.
# 即默认可以使用根分区的全部容量.
# 其他文件系统的话,这里要创建 loopback 设备,所以才需要指定大小.
Size in GB of the new loop device (1GB minimum) [default=91GB]: 100G
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]:
What should the new bridge be called? [default=lxdbr0]:
What IPv4 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
What IPv6 address should be used? (CIDR subnet notation, “auto” or “none”) [default=auto]:
Would you like LXD to be available over the network? (yes/no) [default=no]: yes
Address to bind LXD to (not including port) [default=all]:
Port to bind LXD to [default=8443]:
Trust password for new clients:
Again:
# 最后打印输出配置的内容,以便检查是否有误
Would you like a YAML "lxd init" preseed to be printed? (yes/no) [default=no]: yes
将管理员用户加入lxd组:
$ sudo gpasswd -a $(whomai) lxd
Note: 将一个用户加入lxd用户组相当于给该用户容器管理的root权限。Docker的docker用户组也是这样的。在运行相关命令时就可以不加sudo
了。
换源和拉取镜像
$ lxc remote add tuna-images https://mirrors.tuna.tsinghua.edu.cn/lxc-images/ --protocol=simplestreams --public
$ lxc image copy tuna-images:ubuntu/20.04 local: --alias ubuntu/20.04 --copy-aliases --public
配置idmap
为方便权限映射和管理,我们还要配置idmap。首先查看/etc/subuid
和/etc/subgid
这两个文件的内容,两个文件内容应该是一样的,默认为:
lxd:100000:65536
ubuntu:100000:65536
这表示将允许用户ubuntu使用宿主机中的uid/gid
从100000开始的65535个id,用于容器内的分配。
- LXD 的默认运行方式,即在非特权模式下运行。容器内的用户无法逃逸到宿主机,对宿主机造成破坏。
- 我们想要让宿主机里的用户 alice 和容器中的 ubuntu 用户相互映射,从而实现宿主机和容器的文件共享,并保证宿主机中的文件权限不会混乱。此时就可以使用 LXD 的
raw.id
配置了。为此,将/etc/subuid
和/etc/subgid
文件中的lxd
和root
两行都删掉,重新添加以下四行:
lxd:1000:1000
ubuntu:1000:1000
lxd:100000:10000000
ubuntu:100000:10000000
这里我们把原来使用的id数量增加了,这是为了嵌套使用容器而设置的,可以在lxc 容器中嵌套使用lxc容器或docker容器。POSIX要求可用的id至少要有65536个。然后,我们还设置允许用户ubuntu使用uid/gid
从1000开始给的1000个id。正好我们在宿主机创建的uid/gid
默认是从1000开始递增的。修改/etc/subuid
和/etc/subgid
需要重启lxd服务才能生效:systemctl restart lxd
。
Note: 这里的 idmap 的设置是为了方便管理,管理员也可以考虑跳过。只需要给不同的容器在宿主机上分配不同的目录挂载进去给用户使用,不要允许用户访问宿主机,也没有。不过好像这样子管理员在宿主机上也看不出来到底是哪个用户在占用显存了。在宿主机上用 nvidia-smi
命令查到进程的 pid,再根据 pid 去查看对应的用户的 uid 应该就都是一样的。为了管理方便,还是多做点吧。
容器基础配置
# 设置conda 共享目录,实现容器共用conda环境
$ lxc profile device add default conda disk {source,path}=/opt/anaconda3
# 添加GPU
$ lxc profile device add default gpu gpu
# 配置nvidia.runtime, 让容器使用宿主机的驱动和相关runtime。不需要在容器内安装驱动
$ lxc profile set default nvidia.runtime true
# 开机自动启动容器
$ lxc profile set default boot.autostart true
# 将系统的/etc/profile 复制一份用来设置cuda相关的共享文件
$ sudo cp /etc/profile /etc/lxd_container_public_profile
# 设置 profile 共享文件,便于共享cuda
$ lxc profile device add default container_etc_profile disk source=/etc/lxd_container_public_profile path=/etc/profile
# 可以再次检查这个默认的profile是否错误
$ lxc profile show default
添加CUDA配置文件
# 创建名为 cuda 的配置文件
$ lxc profile create cuda
# 挂载 cuda
$ lxc profile device add cuda cuda-11.1 disk {source,path}=/usr/local/cuda-11.1
# 再次检查是否有误
$ lxc profile show cuda
创建容器模板
# 创建容器模板 template
$ lxc init ubuntu/20.04 template -p default
# 启动容器
$ lxc start template
# 连接容器
$ lxc exec template bash
进入容器后,安装软件并进行配置(默认进入的是容器的root环境)
# 更新源并安装软件
apt update
apt install git tmux htop gcc g++ build-essential net-tools -y
# 为了实现共享conda环境,配置用户组和用户信息
# 新建用户组
groupadd conda_public
# 将当前用户添加到用户组
gpasswd -a ubuntu conda_public
# 便于环境变量加载,在ubuntu用户的用户目录下进行配置。在其中添加`source /etc/profile`
vim /home/ubunut/.bashrc
# 退出容器环境
exit
为方便在容器中使用CUDA中的nvcc
,添加这个配置文件,并绑定路径
# 为template容器添加cuda配置文件
$ lxc profile add template cuda
# 绑定路径便于立即生效
$ lxc exec -t template findmnt cuda
保存容器为镜像模板
# 删除 apt 缓存,减小镜像大小
rm -rfv /var/lib/apt/lists/*
# 退出并停止容器
exit
$ lxc stop template
# 保存容器为镜像模板
$ lxc publish template --alias template-ubuntu-20.04-gpu
这里我们将容器保存为镜像模板,并且设置该镜像的别名为 template-ubuntu-18.04
。以后,只需从该模板创建容器给用户使用即可。
基于模板创建用户容器
例如,给用户alice创建一个容器
$ lxc launch template-ubuntu-20.04-gpu alice-container
从方便管理的角度考虑:
- 我们在宿主机也创建用户 alice,但是不设置密码,同时将其 SHELL 设置为
nologin
,alice 用户就无法登录到宿主机。 - 同时我们把宿主机 alice 用户的 home 目录挂载到容器内,并设置好权限,使得宿主机中的用户 alice 与容器内的用户 ubuntu 映射起来,从而实现容器内的用户 ubuntu 可以读写挂载进来的 alice 的 home 目录。这样即可以保证容器内的用户可以将数据持久化存储到宿主机挂载进来的目录,而在宿主机这边管理员也可以很好的管理和区分不同用户的文件。
# 在宿主机创建用户 alice
$ sudo useradd -m -s $(which nologin) alice
# 将用户添加到conda_public用户组,获取cuda和conda共享目录的读写权限
$ sudo useradd -a -G conda_public alice
# 将宿主机中用户 alice 的 home 目录挂载到容器内的 /home/ubuntu 目录去
# 这样子用户的所有数据都会保存到宿主机上的 /hom/alice 目录,容器出错直接删除即可,数据也不会丢失.
$ lxc config device add alice-container home disk source=/home/alice path=/home/ubuntu
# 设置该容器的 idmap,让宿主机的 alice 用户可以映射到容器内的 ubuntu 用户
# idmap设置参考:https://github.com/lxc/lxd/blob/master/doc/userns-idmap.md
$ printf "both $(id alice -u) 1000\ngid 1101 1000" | lxc config set alice-container raw.idmap -
# 检查 raw.idmap 的设置
$ lxc config get alice-container raw.idmap
# 应该得到类似这样的输出:
both 1001 1000
gid 1002 1001
# 第一行说明:宿主机的 alice 用户的 uid 和 gid 都分别映射到容器内的 ubuntu 用户的 uid 和 gid 了
# 第二行说明:宿主机的conda_public 用户组的gid 映射到容器内的conda_puiblic用户组的gid了
# 也就是说,容器内 ubuntu 用户的权限等价与宿主机里 alice 用户的权限
# 从而可以读写挂载进容器的 alice 用户的 home 目录
进入用户容器进行配置:
# 进入用户容器
$ lxc exec alice-container bash
通过passwd
命令设置root用户ubuntu账户密码
Note: ubuntu为容器的一个默认用户
查看网络信息
# 查看当前ip
$ ifconfig
# 查看网关信息
(base) root@alice-container:~# netstat -rn
Kernel IP routing table
Destination Gateway Genmask Flags MSS Window irtt Iface
0.0.0.0 10.55.146.1 0.0.0.0 UG 0 0 0 eth0
10.55.146.0 0.0.0.0 255.255.255.0 U 0 0 0 eth0
为防止容器重启发生IP变化,修改/etc/netplan/10-lxc.yaml
文件设置静态IP
network:
version: 2
ethernets:
eth0:
dhcp4: false
dhcp-identifier: mac
addresses: [10.55.146.249/24]
optional: true
gateway4: 10.55.146.1
nameservers:
addresses: [8.8.8.8, 114.114.114.114]
因公用计算资源为云服务器,为实现让每个同学都可以通过SSH连接自己的容器,配置端口转发
$ lxc config device add alice-container sshport proxy listen=tcp:0.0.0.0:6022 connect=tcp:10.55.146.249:22 bind=host
alice同学就可以通过SSH连接自己的容器了
$ ssh -P 6022 ubuntu@宿主机ip
至此用户的容器配置完成,若有新的用户需要使用公用gpu云服务器的算力,可基于模板创建用户容器
当然还可通过SSHFS远程挂载文件夹,方便远程coding。
网友评论