亚马逊AWS官方博客

玩转GPU实例 – 我的Linux 工具箱

前言

显卡意味着什么?在不同的玩家心目中会有许多不同的答案。对我来说,开始着迷于显卡要从横空出世的Voodoo说起。那时候显卡市场的王者3dfx 推出的一系列产品无论是游戏画质还是高分辨率下的流畅度都是其它产品望尘莫及的,市场的份额曾经高达85%。只是那时候我的认识中显卡只是与游戏、视频输出效果这些场景有关,没有想到后来的所谓GPU居然有了今天的局面。至于后来NVIDIA Force的卧薪尝胆、ATI Radeon的惊艳亮相以及Matrox、S3等刹那的辉煌……这些都让每一个玩家难以忘怀。

大约十年前,在饭桌上听同事说起他在读博期间参与的项目,提到了使用 CUDA开发来以提高浮点运算的速度。那时候只是觉得所谓的高性能计算(HPC)距离我们还很远,Nvidia 的显卡用来加速运算听起来固然有趣,但更多的只是饭桌上的谈资。哪里预计得到今天的大红大紫。

至于AWS上的GPU实例最早要上溯到2015年。那一年发布的EC2 G2 实例第一次为开发者提供了云计算上的GPU服务。四块NVIDIA GRID GPUs 显卡提供的处理能力,让我们可以真正体会GPU的实力。而随着机器学习尤其是深度学习的快速发展,又进一步加速了这个领域的发展。2016年9月,构建于NVIDIA® Tesla® K80之上的EC2 P2实例面世了。高达16块显卡的配置,不免让我们对于机器学习的发展有了更多的期望。

显然,硬件的发展还是没有及时跟上算法的进步与数据膨胀。看着明显老迈的P2实例难免让我心生焦虑。幸好,2017年10月的P3实例在千呼万唤中飘然而至。8 个NVIDIA Tesla V100 GPU、Intel Xeon E5 处理器的 64 个 vCPU、488GB RAM 以及采用 Elastic Network Adapter 技术、高达 25Gbps 的聚合网络带宽显然成为了GPU实例中的王者。当然仅有豪华的硬件配置是不可能解决全部的问题,还需要每个开发者充分利用好这一资源平台。在P3之后发布的P3dn 以及接下来即将到来的P4实例,将会提供更强大的计算平台,但这都需要每个使用者的精巧构思,力求物尽其用方可尽显GPU的强大的能力。

去年底因为参加 NVIDIA GTC 大会的需要,又一次连接上了我的 GPU实例,又不得不重复以往做过许多次的工作,安装、配置、编译、优化 … 这些繁琐的操作突然感觉自己在不断重复之中似乎缺少了点什么。于是心生念头,将曾经在GPU实例上的心得写成脚本以利于今后工作之用。这些脚本会涵盖曾经尝试过的一些内容 :

  • GPU实例的创建与管理
  • GPU实例的基础配置
  • 实例的系统优化
  • 实例的网络优化
  • Intel 软件的安装配置
  • OpenCV 编译安装
  • 开发工具篇
  • Nvidia 软件篇
  • Jupyter 的安装配置
  • OpenMPI 编译与配置
  • Horovod 配置
  • 深度学习框架篇(Tensorflow、PyTorch以及Mxnet)

在这个技术高速发展的年代,个人的努力是非常渺小的。我的这些心得与积累或有不足甚至谬误之处。非常希望听到更多的反馈与建议,也只有群智群力才能使得我们曾益其所不能。

 

第一部分 : GPU 实例的创建与管理

使用过AWS 的用户都应该有过创建EC2实例的经验。我们常用的方法不外乎AWS控制台、AWS 命令行工具、CloudFormation 模版工具以及第三方的运维工具(Terraform、puppet、ansibley以及chef等)。从我的经验来看,AWS命令行工具(awscli)应该是最麻烦的一个了。原因就在于我们需要熟练的掌握的参数实在是太多了,请看完整的awscli 中创建实例的命令run-instance 的完整参数 :

创建实例的脚本

相信对一个普通人来说这绝对是一个不能能完成的任务。但是,事情的另一面却是AWS命令行工具(awscli ) 提供给我们的绝对是一个强大的、可以随心所欲进行定制的工具。用好这个工具的一个简单而有效的方法就是脚本。毫不夸张的说,一个好的脚本带给我们的价值的巨大的。它不仅可以节省我们大量的重复性的工作的时间,还可以以一种灵活的、程序化方式满足各种各样的运维的需求。而创建一个EC2的GPU实例就属于这一类的范畴。好了,我的创建实例的脚本就是这个样子的

 

#!/bin/bash



instance_type="p3.16xlarge"

key_name="密钥名字"

security_group_ids="安全组ID"

subnet_id="子网ID"

placement="置放群组"

block_device_mappings=""



#http://169.254.169.254/latest/user-data/

user_data="ubuntu_userdata.txt"

ebs_mapping="ebs_mapping.json"

count="数量"

region="AWS区域"



#ubuntu 18.0.4 LTS

image_id=$(aws ec2 describe-images --owners 099720109477 --filters \

'Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server*' \

'Name=state,Values=available' 'Name=architecture,Values=x86_64' --query \

'reverse(sort_by(Images, &CreationDate))[:1].ImageId' --output text --region ${region})



owner="使用者"

current_date_time="`date +%Y%m%d%H%M`";

project="项目名称"

tags="ResourceType=instance,Tags=[{Key=owner,Value=$owner},{Key=date,Value=$current_date_time},{Key=project,Value=$project}] ResourceType=volume,Tags=[{Key=owner,Value=$owner},{Key=date,Value=$current_date_time},{Key=project,Value=$project}]"



# Using --dry-run to test

INSTANCE_ID=$(aws ec2 run-instances \

--image-id ${image_id} \

--count ${count} \

--instance-type ${instance_type} \

--key-name ${key_name} \

--security-group-ids ${security_group_ids} \

--subnet-id ${subnet_id} \

--ebs-optimized --associate-public-ip-address \

--block-device-mappings "file://${ebs_mapping}" \

--user-data "file://${user_data}" \

--region ${region} \

--tag-specifications ${tags} \

--output text --query 'Instances[*].InstanceId'

)



#aws ec2 wait instance-status-ok \

aws ec2 wait instance-running \

--instance-ids ${INSTANCE_ID} --region ${region}



IP_ADDRESS=$(aws ec2 describe-instances \

--instance-ids  ${INSTANCE_ID} \

--query "Reservations[*].Instances[*].PublicIpAddress" \

--region ${region} \

--output=text)



echo "The instance is availiable now, access with : ssh ubuntu@${IP_ADDRESS}."

echo "Done."

 

脚本中的参数

这段脚本的内容并不复杂。理解其中的几个关键的变量就可以灵活的配置使用。其中需要提前准备几个重要的变量是-

  • 实例的类型

以P3 实例为例,P3实例提供了三种实例大小:带有 1 个 GPU 的2xlarge,带有 4 个 GPU 的 p3.8xlarge 以及带有 8 个 GPU 的 p3.16xlarge。脚本中可酌情选择,例如:
instance_type=”p3.16xlarge”

 

  • 密钥对的名字

可以通过控制台、命令行创建。也可以将自己创建的公有密钥上传到将要使用的AWS区域上。关于这部分内容可以参考AWS文档,例如:
key_name=”id_rsa”

 

  • 安全组的ID

当我们创建实例时,以为该实例最多分配 5 个安全组。安全组是被用来控制到实例的入站数据流,以及另外一套单独规则以控制出站数据流。关于安全组的细节,请参考这里。在这里,我们的安全组设定需要开放SSH的端口以便于连接使用。例如:
security_group_ids=”sg-0f4bb098276d25df8″

 

  • 实例所属的的子网ID

子网是VPC 内的 IP 地址范围,每个实例都要归属到一个字网当中。例如:
subnet_id=”subnet-06c44af45fcd8512f”

 

  • 置放群组

设置置放群组。这里提到的置放群组是放置实例的一种方式。针对深度学习,尤其是多节点分布式模型训练的场景,我们选择的是集群置放群组。这意味着,通过将一个可用区中靠近的实例打包在一起。可以实现所需的低延迟网络性能,以满足分布式模型训练通常使用的紧密耦合的节点到节点通信的要求。关于置放群组,可以通过这里了解更多。例如:
placement=”GroupName = DL-pg”

 

  • 块设备映射项

这里不需要使用这项设置
例如:block_device_mappings=””

 

上述的这些项目中,密钥对、安全组、VPC子网以及置放群组需要预先设置好。此外,还需要了解以下几个重要的参数:

  • 用户数据

这个参数是要提供给实例的用户数据。在实例启动的时候,用户数据会被自动执行,通常用来帮助我们完善实例的构建,例如安装/升级程序包等。需要注意的是,用户数据在被执行的过程中是不能够进行与用户的交互的。在官方的文档中,并没有设计Ubuntu 的用户数据样例。因此构建一个没有交互的自动执行的用户数据是非常关键的一步。在我的实践中,这样的一个脚本是可以很好的被实例所执行。

#!/bin/bash
set -e -x
export DEBIAN_FRONTEND=noninteractive
apt-get update &&
    apt-get -o Dpkg::Options::="--force-confold" upgrade -q -y --force-yes &&
    apt-get -o Dpkg::Options::="--force-confold" dist-upgrade -q -y --force-yes
apt-get -y autoremove
apt-get -y install awscli ec2-instance-connect git chrony screen

curl http://169.254.169.254/latest/user-data/ -o /home/ubuntu/userdata.sh
chmod +x /home/ubuntu/userdata.sh
mkdir -p /home/ubuntu/Projects
chown ubuntu:ubuntu /home/ubuntu/Projects
mkdir -p /home/ubuntu/Downloads
chown ubuntu:ubuntu /home/ubuntu/Downloads
echo  "#---------------------¬" >> /home/ubuntu/.bashrc

 

这段脚本完成的任务有 通过apt-get update、apt-get dist-upgrade 完成的系统与软件包的升级;常用软件的安装apt-get -y install awscli ec2-instance-connect git chrony screen;以及创建我们后续将要使用到的一些目录。我们也可以按照自己的需要进行合理的增减。要注意的一点就是不要有任何需要交互的操作,否则这个userdate 将不会被正确的执行。另外userdata 的大小需要控制在16K之内。

 

  • 实例数量

创建的实例的数量,这对于需要同时创建多个同样实例的场景非常用用。例如分布式训练等。例如:
count=”5″

 

  • AWS 区域

这里所谓的区域都是一个单独的地理区域。“区域”对于理解AWS的基础设施是非常重要的一个概念。如果需要更多的了解,需要参考这里。对于每一个区域都有对应的代码。例如 中国(北京)区域 的代码为cn-north-1;中国(宁夏)区域的代码为cn-northwest-1。

 

关于 Ubuntu 18.0.4

事实上,我们在使用一个GPU实例的时候(例如P3实例)会有许多个Linux 分发版本的选择,例如Amazon Linux 2 、Centos 以及Ubuntu等等。但是不得不强调的就是NVIDA 的CUDA 对于众多Linux 的分发版本来说支持最好的莫过于Ubuntu。我曾经大费周折的试图在Debian Stretch 上为我的GTX 1070 安装最新版本的CUDA 。但是屡经挫折之后不得不回到了Ubuntu之上。按照Canonical (Ubuntu的开发商)的声明,Ubuntu 的下个月即将发布的Ubuntu 20.04 LTS 将会集成NVIDIA 私有的显卡驱动,这无疑增强了我们继续选用的信心了。

在AWS EC2的实例上安装Ubuntu 18.04 不是件困难的事情。在AWS 推荐的快速启动的操作系统清单中就提供了Ubuntu18.04 的选项。

但是,不好的地方在于每一个操作系统的镜像(AMI)都需要有一个AMI 的ID。例如美国俄勒冈区域的Ubuntu 18.04 的64位X86的AMI ID为ami-0d1cd67c26f5fca19。但是不同的AWS 区域当中的Ubuntu 18.04 的AMI ID确是完全不同的。这里有一个小的技巧可以帮助我们简单的获取每个区域的Ubuntu 18.04,只需要提供区域的代码即可。

image_id=$(aws ec2 describe-images --owners 099720109477 --filters \
'Name=name,Values=ubuntu/images/hvm-ssd/ubuntu-bionic-18.04-amd64-server*' \
'Name=state,Values=available' 'Name=architecture,Values=x86_64' --query \
'reverse(sort_by(Images, &CreationDate))[:1].ImageId' --output text --region ${region})

 

这里的关键是使用了不同AMI提供着的owner这个参数以及不同的AMI的描述信息。同样的方法也适用于其它的操作系统,例如Amazon Linux 2等。

 

标签(Tag)的用法

对于AWS 资源打标签(tag)绝对是一个非常有用但很容易被忽视的地方。那么什么是“标签”呢?标签是指为 使用的AWS 资源分配的标记。每个标签都包含我们自行定义的一个键和一个值。标签可让我们灵活的按照各种标准 (例如项目、用途、所有者) 对 AWS 资源进行分类。这在具有大量相同类型的资源时将会很有用的功能 — 可以根据分配给资源的标签快速识别特定资源。例如,您可以为不同项目的 Amazon EC2 实例定义一组标签,以跟踪不同项目实例的使用情况以及成本的状况。简单的使用方法如下:

owner="使用者名字"
current_date_time="`date +%Y%m%d%H%M`";
project="项目名称"
tags="ResourceType=instance,Tags=[{Key=owner,Value=$owner},{Key=date,Value=$current_date_time},{Key=project,Value=$project}] ResourceType=volume,Tags=[{Key=owner,Value=$owner},{Key=date,Value=$current_date_time},{Key=project,Value=$project}]"

这里标签的名称与标签完全是由我们自行定义的。在上面的这个例子中,就定义了实例的使用者、实例建立的日期、实例所属的项目的名称等。

网络存储(EBS)的设定

Amazon Elastic Block Store (EBS) 是AWS提供的一种数据块存储服务,通常与EC2一起使用。对于我们即将创建的实例这是一种非常适用的存储方式。设定实例中所使用的EBS的配置,需要在一个配置文件中声明。例如:

[{
    "DeviceName": "/dev/sda1",
    "Ebs": {
        "DeleteOnTermination": true,
        "VolumeSize": 64,
        "VolumeType": "gp2",
        "Encrypted": false
    }
}]

在这个配置中,“DeleteOnTermination”声明了当实例终止时需要删除该存储卷;
“VolumeSize”设置的是存储容量的大小,单位是GB;“VolumeType”需要在高性能的io1、通用型的gp2、吞吐量优化的st1以及最低成本的sc1 四种类型中进行选择。关于这四种类型的差异可以通过这张表来一窥究竟

最后的设置项是关于数据加密。“Encrypted”用来声明存储在EBS上数据是否以加密方式存储。无疑,选择加密存储会很好的保护我们的隐私数据。

 

创建实例与连接到实例

最后的一个步骤就是利用aws ec2 run-instances命令在我们准备好的参数与配置项之上创建我们需要的实例。并且为了后续操作的方面,会将实例绑定Public IP 显示出来。

IP_ADDRESS=$(aws ec2 describe-instances \

--instance-ids  ${INSTANCE_ID} \

--query "Reservations[*].Instances[*].PublicIpAddress" \

--region ${region} \

--output=text)

echo "The instance is availiable now, access with : ssh ubuntu@${IP_ADDRESS}."

echo "Done."

 

这样我们就可以利用ssh 命令登陆到这台实例上面。但是,对我们而言长时间的记住一组IP地址显然是不切实际的。我的工作习惯是准备一组关于EC2使用的Linux 的别名(在我的.bashrc 文件中定义)。通过这些别名/命令来帮助我们找到实例,甚至是帮助我们关闭或者启动实例。

alias ec2_list='aws ec2 describe-instances --output table –query '\''Reservations[*].Instances[*].[InstanceId,ImageId,State.Name,PublicIpAddress,Tags[*].Value | [0]]'\'

alias ec2_start='aws ec2 start-instances --instance-ids'

alias ec2_stop='aws ec2 stop-instances --instance-ids'

alias ec2_terminate='aws ec2 terminate-instances --instance-ids'

 

只需要在命令行输入ec2_list –region cn-northwest-1 ,就能够看到在我的账户在中国(宁夏)区域上的全部EC2 实例,包括该实例的Public IP。

到这里我们的GPU实例应该已经创建完成。接下来我们要对这台实例进行细致的优化与配置,我将在该系列的下一篇继续这项工作。

 

 

本篇作者

费良宏

费良宏,AWS Principal Developer Advocate。在过去的20多年一直从事软件架构、程序开发以及技术推广等领域的工作。他经常在各类技术会议上发表演讲进行分享,他还是多个技术社区的热心参与者。他擅长Web领域应用、移动应用以及机器学习等的开发,也从事过多个大型软件项目的设计、开发与项目管理。目前他专注与云计算以及互联网等技术领域,致力于帮助中国的 开发者构建基于云计算的新一代的互联网应用。