为实验室构建公用GPU服务器
需求的形成
如今深度学习风生水起,为了满足人民日渐下降的估算需求,相信各个实验室都开始买起了主板。但是虽然主板还是贵,做不到人手一块,所以只能以公用机器的方式出现了。
你们都在公用机器上跑实验,而各自所须要的软件(例如Cuda、……)版本却可能不一样,这样很容易由于版本问题而造成程序难以运行。解决这种软件冲突问题是一个又冗长又历时的工作,并且经常弄得鸡飞狗跳,最终没有人可以正常运行实验。所以说,我们希望在公用的机器上才能有一定的管理,促使不同的用户不会互相影响。
这儿我列举一些需求:
下边我将表述我解决以上需求的方式,以供有须要的人参考。本文的受众应当是实验室的公用机器管理员,有一定的Linux基础,或则对此感兴趣的普通用户。
解决思路
从需求出发,首先解决的问题就是如何做用户的隔离。最简单粗鲁的方式无疑就是虚拟机,但是现今的家用主板并不支持虚拟化,而且CPU虚拟化的额外开支还是很可观的,另外IO虚拟化的性能更是问题。
目前好多都支持PCI,但是若果采用这个技术的话,主板就只能被一台虚拟机独占,其他虚拟机就没法使用这块主板。有趣的是,通常的代码是远远未能占满GPU的,GPU借助率只会达到10%~30%左右。在这样的情况下,多个用户共享同一个GPU是合理的,也是提升硬件资源的借助率。
与之类似的就是CPU和显存资源的界定。使用虚拟化以后,一台虚拟机的CPU和显存基本上是定死的。但是,有时侯你们须要用多一点的CPU,有时侯则不须要这么多;有时侯你们须要用巨多的显存,而有时侯又只须要一点点。无疑这些定死的策略也是增加了硬件资源的借助率。
所以说我们完全不考虑虚拟化。
由于实验室公用机器是一个相对安全和可控的环境,没有安全里面的疑虑,但是现今主流的深度学习平台都是在Linux上,所以我们可以直接借助Linux内核提供的隔离机制来解决这个问题。由于只是做隔离,这儿带来的额外开支微乎其微。但是由于硬件资源都是共享的,这样才能尽可能地借助硬件资源。
拿来做隔离的方式有好多,成熟又出名的就有//LXD/LXC等等。由于太过复杂就不考虑,LXD觉得跟LXC差不了多少。所以我真正用来比较的是和LXC。
现今用的特别多,用上去也十分便捷。而LXC自从出现以后便越来越少地被提到了。仔细考虑和LXC的哲学和应用场景。更倾向于布署应用,更倾向于无状态;而LXC则相反,它想让人把它当作一台虚拟机来使用。再想想实验室的需求,我们须要的是让每位人有一个“虚拟机”,而不是每位人布署一个个应用。
固然,不顾的哲学,硬要把当成虚拟机来用也不是不可以。但是这样一来,的插口反倒成为一种碍眼。再说,原来的不正是LXC里面的一层封装吗?那既然我们的需求更符合LXC的哲学,那为何不直接用LXC呢?
所以我们就选取了LXC。
接出来是我们十分关心的GPU问题,在LXC容器中能使用GPU吗?好在Linux有硬件即文件的哲学,我们只要把宿主机中主板设备对应的文件挂载到LXC容器中能够解决这个问题。
由于访问实验室nas的需求十分普遍,让每位用户各自联接到NAS无疑不是一个好的选择,一是对用户来说麻烦,二是开了好多冗余的联接。所以说,我们可以在宿主机中把NAS挂载好,之后同样地把挂载好的目录挂载到LXC容器中。
要限制用户不能直接操作宿主机,很自然地想到我们可以让用户使用ssh步入到LXC容器中。但是这儿就有几个问题要解决:
用户的LXC容器可能未启动,如何使它启动呢?一个方式自然还是让用户登录到宿主机来启动LXC容器,但是正如上面所说,这与我们的需求矛盾。另一个方式是协程所有LXC容器的状态,假如没有启动就手动启动。这个思路我觉得不是很甜美。
我的解决方式是编撰一个脚本,把它作为用户在宿主机上的Shell。在脚本中检测用户的LXC容器是否启动。若果没有,就启动它。顺带地,脚本还可以输出更多的信息,提示用户一些常用的操作。这样一来就解决了这个问题,但是由于用户的Shell早已不是传统的/bin/bash等,而是我们特制的脚本,所以用户在ssh步入宿主机以后只能执行这个脚本,而不能执行别的命令。这就挺好地制止了用户直接操作宿主机。
LXC容器的IP是只有宿主机能访问得到的外网IP,用户要如何访问?有一个方式是先让用户登录宿主机,之后以宿主机作为跳板步入LXC容器。但是这样一是对用户来说麻烦,二是这样用户能够直接操作宿主机了,与我们的需求矛盾。
我的解决方式是给每位用户分配一个端口,借助把这个端口转发到对应LXC容器的22端口。这样用户使用这个分配的端口联接宿主机,就相当于联接容器的22端口。
具体地说如何在脚本上面得到这种端口呢?我的方式是在//next-port上面保存一个数字,即下一个新用户的端口,每次新增用户的时侯递增。而每位用户的端口,则是保存在//ports/$USER。
为了便捷用户使用,管理员须要编撰一点简单的文档指引用户。反过来,管理员也得给自己写一点简单的脚本来便捷添加删掉用户这样的操作。
方式概述
下边总结一下前面提及的方式:
在宿主机上安装主板驱动,把主板设备文件挂载到LXC容器中在宿主机上挂载NAS,再把挂载好的路径挂载到LXC容器中编撰一个脚本作为宿主机上用户Shell,这个脚本应当要能做启动、关闭LXC容器等操作为用户编撰简单的使用说明为管理员编撰用于添加、删除用户的脚本宿主机的预先配置
宿主机首先须要装好主板驱动,使用ls/dev/*查看相关的设备文件,使用-smi命令确保主板驱动正常运行。有趣的是,/dev/-uvm这个设备文件并不会自己创建。我们可以用这儿提供的脚本来解决这个问题。把这个脚本设置成开机自启动(譬如说简单粗鲁地加在/etc/rc.local上面)。
宿主机的NAS挂载也要配置好。我们的NAS支持NFS合同,所以直接在/etc/fstab上面添加一些内容就好了。
在前面的用户Shell脚本中须要用到sudo,我们不希望让用户再度输入密码,所以我们在上面设置成不须要使用密码。
host$ sudo vim /etc/rc.local # for the /dev/nvidia-uvm script
host$ sudo vim /etc/fstab
172.16.2.30:/mnt/NAS/Share /NAS/Share nfs rw 0 0
host$ sudo mount -a
host$ sudo service lightdm stop # if Ubuntu Desktop
host$ sudo sh /NAS/Share/GPU_Server/NVIDIA-Linux-X86_64-375.20.run
host$ ls /dev/nvidia*
/dev/nvidia0 /dev/nvidiactl /dev/nvidia-modeset /dev/nvidia-uvm
host$ nvidia-smi # should have no error
host$ sudo visudo
%sudo ALL=(ALL:ALL) NOPASSWD:ALL
制做LXC容器模板
为了便捷添加用户,我们先制做一个LXC容器模板,然后每次新建容器的时侯就从这个模板克隆一份。
首先管理员在宿主机上使用自己的普通权限帐号新建一个LXC容器,这儿可以跟随LXC官方的文档进行操作。其中,对于中国用户来说,可以使用北大的镜像来加速镜像的下载。在这儿,我把容器的名子就称作,前面添加用户的脚本有时会对这个名子进行替换(例如替换/etc/hosts之类的)。
在启动容器之前,我们须要先更改容器的配置文件,把主板设备文件挂载进去,另外由于我们主板驱动的安装程序放到了NAS上,所以顺带也把NAS挂上。
之后我们启动容器,再使用lxc-attch步入容器,安装一些额外的软件(例如说-),以及做一些额外的配置(例如说把软件源换成校内源)。
接着我们须要在容器中安装好主板驱动。其实,这一步完全可以留给用户做,但是由于宿主机和容器内的主板驱动要求完全一致,所以我们索性替用户做了,省得出现问题。在安装驱动的时侯会提示难以卸载内核模块,这是正常的,虽然容器和宿主机是共享内核的。实际上,我们在容器内也不须要安装内核模块,只是须要这些库罢了,所以在安装主板驱动的时侯带上--no--就可以解决这个问题。
配置好了模板容器以后,我们关掉这个容器,并把它复制到/root/lxc--/,但是稍为更改其中的配置文件,把work.,lxc.,lxc.,lxc.等容器特有的配置删掉。这种配置我们在前面的添加用户的脚本中再把它们生成下来。
host$ sudo apt install lxc
host$ sudo vim /etc/lxc/lxc-usernet
host$ lxc-create -t download -n template -- --server mirrors.tuna.tsinghua.edu.cn/lxc-images
host$ vim ~/.local/share/lxc/template/config
lxc.mount.entry = /dev/nvidia0 dev/nvidia0 none bind,optional,create=file
lxc.mount.entry = /dev/nvidiactl dev/nvidiactl none bind,optional,create=file
lxc.mount.entry = /dev/nvidia-modeset dev/nvidia-modeset none bind,optional,create=file
lxc.mount.entry = /dev/nvidia-uvm dev/nvidia-uvm none bind,optional,create=file
lxc.mount.entry = /NAS/Share NAS/Share none bind,create=dir
host$ lxc-start -d -n template
host$ lxc-attach -n template
container# vim /etc/apt/sources.list
container# apt update && apt install -y openssh-server
container# sh /NVIDIA-Linux-x86_64-375.20.run --no-kernel-module
container# nvidia-smi
container# exit
host$ lxc-stop -n template
host$ sudo mkdir -p /root/lxc-public-images/
host$ sudo cp -r ~/.local/share/lxc/template /root/lxc-public-images/template
host$ sudo vim /root/lxc-public-images/template/config
# delete: lxc.network.hwaddr, lxc.id_map, lxc.rootfs, lxc.utsname
编撰各类脚本
我把所有须要用到的脚本都放到了。这是用于我们实验室的脚本,假如你须要借鉴,切勿直接复制粘贴,请确保自己明白每一行命令的作用,之后做更改。
编撰用户文档
这儿有一份我们实验室里的GPU使用手册可以借鉴。
结语
这一套流程完整地做出来确实十分折腾。不过有了那么一套简单的管理方式以后,至少你们就不用由于软件冲突而大大增加工作效率。似乎只提供了ssh,但只要有了ssh,就可以控制远程笔记本、传输文件、转发X11图形界面、使用sshfs把远程文件系统挂载到本地。这基本能够满足通常用户的所有需求了。我之前也写过一篇博客来介绍SSH基本用法,有须要的可以瞧瞧。
至于后续的升级和维护嘛,这就是一个更大的坑了。我只能希望Cuda在推出新版本的同时,不要大幅度增强所需的主板驱动版本,由于一旦这样的事情发生,就必须更新主板驱动,而更新主板驱动则须要更新宿主机上的驱动以及所有LXC容器的驱动。
另一方面,宿主机最好是能不更新就不更新了。做一次dist-基本上就是得从头来一次。而更新内核也是很危险的事情,要重新在宿主机上装一遍主板驱动不要紧,怕的是更新以后和LXC出现了一些兼容性问题,致使LXC容器未能启动。
最后发一张用户登录LXC容器的图片。