亚马逊AWS官方博客

在基于 AWS Inferentia 的 Inf1 实例上部署 TensorFlow OpenPose,借此显著提高资源性价比

Original URL: https://aws.amazon.com/cn/blogs/machine-learning/deploying-tensorflow-openpose-on-aws-inferentia-based-inf1-instances-for-significant-price-performance-improvements/

 

在本文中,我们将使用AWS Neuron编译一套开源的TensorFlow版本的OpenPose,并针对基于AWS Inferentia的实例对推理性能进行调优。与基于GPU的实例相比,我们需要在本演练中设置基准测试环境、衡量图像处理管道的吞吐量,并量化该系统的性价比改进情况。

关于OpenPose

人体姿态估计,属于一类机器学习(ML)与计算机视觉(CV)相结合的技术应用方向,可用于支持行人意图预估、AR和体育赛事中的运动跟踪等多种用例。姿态估计的核心在于识别图像(关节与关键点)的具体坐标,将这些坐标串连起来以构成一个人的骨骼表示。对身体姿态的准确表达,将成为机器人交互设计乃至运动姿态量化等应用目标的实现前提。

目前市面上存在诸多可用于人体姿态估计的方法,其中OpenPose采取深度学习凭借自下而上的方法(由卡耐基梅隆大学认知计算实验室于2018年发布)吸引到众多拥趸。OpenPose是一种多人2D姿态估计模型,其中采用一种称为“部位亲和域”(PAF)的技术对身体各部位进行关联,并借此在图像上建立起多个单独的骨架结构。在这种自下而上的方法中,模型能够识别出各个关键点,并据此将骨架结构拼凑在一起。

为了实现这一目标,OpenPose使用了两步式流程。首先,它使用VGG-19模型提取图像特征,并将这些特征传递至并行运行的一对卷积神经网络(CNN)当中。

其中一套CNN负责计算置信度图,借此检测图像中的各个身体部位。另一套CNN则计算PAF,并将各个部分合并起来,构成人体骨骼结构。您可以多次重复这些并行分支,借此完善置信度图与PAF预测结果。

下图所示,为来自VGG的特征F,此特征被馈送至OpenPose模型的PAF与置信度图分支当中。(来源:使用部位亲和域进行实时多人2D姿态估计

原始OpenPose代码依赖于Caffe模型与预统计的C++库。出于使用便捷性与可移植性的考虑,我们在本轮演练中使用GitHub repo中的tf-pose-estimation基于TensorFlow 1.15重新实现了OpenPose的神经网络。此repo还提供ML管道脚本,大家可以用OpenPose对图像及视频进行预处理与后处理。

先决条件

在本轮演练中,您需要准备一个有权访问AWS管理控制台的AWS账户,同时保证有权限使用公开IP创建 Amazon Elastic Compute Cloud (Amazon EC2)实例和创建Amazon Simple Storage Service Amazon S3)存储桶。

另外,如果您熟悉如何使用AWS Deep Learning AMI与Jupyter notebooks中的Conda环境则更好,但并非硬性要求。

关于AWS Inferentia与Neuron SDK

AWS Inferentia 芯片是一款由AWS定制构建,旨在提供更高推理性能的芯片方案,能够立足云端实现最低推理成本,并帮助大家轻松将ML集成至标准应用程序功能当中。

AWS Neuron 是一款软件开发套件(SDK),由编译器、运行时以及配置文件工具共同组成,这些工具可进一步优化Inferentia芯片的ML推理性能。Neuron已经与TensorFlow、PyTorch以及MXNet等流行ML框架顺畅集成,且预装在AWS Deep Learning AMI当中。在AWS Inferentia上部署深度学习模型的具体方式,与其他平台上的类似环境无甚区别,您可以轻松享受到这款芯片带来的性能提升与成本优化。

AWS Neuron GitHub上发布的最新Neuron版本,增加了对更多模型(包括OpenPose)的支持选项,我们将在后文中重点加以介绍。此外,Neuron新版本还将Neuron PyTorch升级到最新稳定版(1.5.1),允许大家在AWS Inferentia上编译并运行更多模型。

使用Neuron SDK编译TensorFlow OpenPose模型

您可以在AWS中设置EC2实例,借此开启模型编译流程。本文建议大家使用z1d.xlarge实例类型,其拥有出色的单核性能与内存容量。在US East(北弗吉尼亚州)区域内,使用AWS Deep Learning AMI(Ubuntu 18.04)的29.0版本(ami-043f9aeaf108ebc37)。此AMI预打包有Neuron SDK以及AWS Inferentia所需要的Neuron运行时。

关于在EC2实例上运行AWS Deep Learning AMI的更多详细信息,请参阅启动并配置DLAMI

在通过SSH接入该实例时,您可以激活 aws_neuron_tensorflow_p36 Conda环境,并将Neuron编译器更新至最新版本。编译脚本的正常运行,依赖于requirements-compile.txt文件中列出的具体要求。关于编译脚本与需求文件,请参阅 GitHub repo。您可以使用以下代码,将其下载并安装至目标环境当中:

source activate aws_neuron_tensorflow_p36
pip install neuron-cc --upgrade --extra-index-url=https://pip.repos.neuron.amazonaws.com
git clone https://github.com/aws/aws-neuron-sdk.git /tmp/aws-neuron-sdk && cp /tmp/aws-neuron-sdk/src/examples/tensorflow/<name_of_the_new_folder>/* . && rm -rf /tmp/aws-neuron-sdk/
pip install -r requirements-compile.txt

 

接下来,我们就可以开始编译过程。您可以编译 tf-pose-estimation网络冻结图,在GitHub repo上可以找到。下面将下载到的原始脚本调整为单行wget命令:

wget -c --tries=2 $( wget -q -O - http://www.mediafire.com/file/qlzzr20mpocnpa3/graph_opt.pb | grep -o 'http*://download[^"]*' | tail -n 1 ) -O graph_opt.pb

 

当下载完成之后,运行convert_graph_opt.py 脚本以为AWS Inferentia芯片编译。由于Neuron属于提前(AOT)编译器,大家需要在编译之前定义特定的图像大小。您可以使用 —net_resolution参数(例如net_resolution=656x368)来调整网络输入图像的分辨率。

编译后的模型可以在推理运行过程中接受任意批次大小的输入。此属性可以对模型的大规模部署进行基准测试;但是,示例 tf-pose-estimation repo中用于图像及视频处理管道的批次大小设置为1。

要开始编译流程,请输入以下代码:

python convert_graph_opt.py graph_opt.pb graph_opt_neuron_656x368.pb

 

编译流程最多可能需要20分钟。在此期间,编译器会优化TensorFlow图运算并为已保存的模型生成AWS Inferentia版本。在编译过程中,系统会保留详尽的编译日志,例如:

2020-07-15 21:44:43.008627: I bazel-out/k8-opt/bin/tensorflow/neuron/convert/segment.cc:460] There are 11 ops of 7 different types in the graph that are not compiled by neuron-cc: Const, NoOp, Placeholder, RealDiv, Sub, Cast, Transpose, (For more information see https://github.com/aws/aws-neuron-sdk/blob/master/release-notes/neuron-cc-ops/neuron-cc-ops-tensorflow.md).
INFO:tensorflow:fusing subgraph neuron_op_ed41d2deb8c54255 with neuron-cc
INFO:tensorflow:Number of operations in TensorFlow session: 474
INFO:tensorflow:Number of operations after tf.neuron optimizations: 474
INFO:tensorflow:Number of operations placed on Neuron runtime: 465

 

在评估编译后的模型性能之前,您需要切换至由AWS Inferentia芯片提供支持的EC2 Inf1实例。要在两个实例之间共享编译完成的模型,请使用以下代码创建一个S3存储桶:

aws s3 mb s3://<MY_BUCKET_NAME>
aws s3 cp graph_opt_neuron_656x368.pb s3://<MY_BUCKET_NAME>/graph_model.pb

 

在AWS EC2 Inf1实例上使用Jupyter notebook对推理时间进行基准测试

在将编译完成的graph_model.pb模型保存在S3存储桶后,我们需要修改GitHub repo上的ML管道脚本,借此根据图像与视频估计人体姿态。

要设置基准Inf1实例,您可以重复以上在z1d实例上配置编译的步骤。大家可以使用相同的AMI,只需要注意将实例类型更改为inf1.xlarge。本文建议大家使用g4dn.xlarge实例,在设置基本不变的情况下,这样的类型选择将帮助我们更直接地将运行在GPU上的基础 tf-pose-estimation模型性能与AWS Inferentia编译模型进行比较。

通过本文,我们将使用Jupyter Lab服务器与目标实例及模型进行交互。关于在Amazon EC2上配置Jupyter Lab的更多详细信息,请参阅如何设置Jupyter Notebook服务器

为tf-pose设置Conda环境

在登录至Jupyter Lab服务器之后,您即可克隆包含有TensorFlow版OpenPose的GitHub repo。

在Jupyter Launcher页面中的Other之下,选择Terminal

在终端内,激活包含有Neuron SDK的aws_neuron_tensorflow_p36 环境。环境激活与克隆操作需要使用以下代码:

conda activate aws_neuron_tensorflow_p36
git clone https://github.com/ildoonet/tf-pose-estimation.git
cd tf-pose-estimation

 

在克隆完成之后,我们建议您按照软件包安装说明进行操作,分步完成repo安装。在同一终端屏幕内,您可以通过安装GitHub repo中requirements.txt 文件所列出的opencv-python与依赖项进行环境定制。

您需要运行两条pip命令:第一条命令负责处理opencv-python;第二条负责完成 requirements.txt中指定的安装步骤:

pip install opencv-python 
pip install -r requirements.txt

 

现在,大家可以开始构建notebook了。

在repo的root目录中,选择NotebookEnvironmentconda_aws_neuron_tensorflow_p36)以创建一个新的Jupyter notebook。在notebook的第一个单元格中,导入run.py脚本中定义的库——此脚本将作为图像处理的参考管道。在后续单元格中,创建一个记录器以记录基准测试结果。详见以下代码:

import argparse
import logging
import sys
import time

from tf_pose import common
import cv2
import numpy as np
from tf_pose.estimator import TfPoseEstimator
from tf_pose.networks import get_graph_path, model_wh

 

logger = logging.getLogger('TfPoseEstimatorRun')
logger.handlers.clear()
logger.setLevel(logging.DEBUG)
ch = logging.StreamHandler()
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('[%(asctime)s] [%(name)s] [%(levelname)s] %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)

 

定义主要推理函数main()与辅助绘图函数plotter()。这些函数是直接从run.py处复制的OpenPose推理管道。我们需要对其做出简单修改,即添加repeats参数,借此依次运行多个推理步骤并改进对平均模型吞吐量的度量能力(以每张图像的处理秒数为单位):

def main(argString='--image ./images/contortion1.jpg --model cmu', repeats=10):
    parser = argparse.ArgumentParser(description='tf-pose-estimation run')
    parser.add_argument('--image', type=str, default='./images/apink2.jpg')
    parser.add_argument('--model', type=str, default='cmu',
                        help='cmu / mobilenet_thin / mobilenet_v2_large / mobilenet_v2_small')
    parser.add_argument('--resize', type=str, default='0x0',
                        help='if provided, resize images before they are processed. '
                             'default=0x0, Recommends : 432x368 or 656x368 or 1312x736 ')
    parser.add_argument('--resize-out-ratio', type=float, default=2.0,
                        help='if provided, resize heatmaps before they are post-processed. default=1.0')

    args = parser.parse_args(argString.split())

    w, h = model_wh(args.resize)
    if w == 0 or h == 0:
        e = TfPoseEstimator(get_graph_path(args.model), target_size=(432, 368))
    else:
        e = TfPoseEstimator(get_graph_path(args.model), target_size=(w, h))

    # estimate human poses from a single image !
    image = common.read_imgfile(args.image, None, None)
    if image is None:
        logger.error('Image can not be read, path=%s' % args.image)
        sys.exit(-1)

    t = time.time()
    for _ in range(repeats):
        humans = e.inference(image, resize_to_default=(w > 0 and h > 0), upsample_size=args.resize_out_ratio)
    elapsed = time.time() - t

    logger.info('%d times inference on image: %s at %.4f seconds/image.' % (repeats, args.image, elapsed/repeats))

    image = TfPoseEstimator.draw_humans(image, humans, imgcopy=False)
    return image, e

 

def plotter(image):
    try:
        import matplotlib.pyplot as plt

        fig = plt.figure(figsize=(12,12))
        a = fig.add_subplot(1, 1, 1)
        a.set_title('Result')
        plt.imshow(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
        
    except Exception as e:
        logger.warning('matplitlib error, %s' % e)
        cv2.imshow('result', image)
        cv2.waitKey()

 

另外,如果您比较喜欢冒险,也可以根据run_video.pyrun_directory.py修改用于推理视频或批量图像的代码结构。本轮演练仅供参考,大家不妨放松心态,尽情尝试!

其中main()函数会将GitHub repo中Test Inference部分描述的参数字符串作为输入。要测试notebook的实现效果,请使用一组参考参数(请使用原始下载脚本以确保下载cmu模型):

img, e = main('--model cmu --resize 656x368 --image=./images/ski.jpg --resize-out-ratio 2.0')
plotter(img)

 

日志内容显示,您的第一个多人姿态分析任务已经完成:

‘[TfPoseEstimatorRun] [INFO] 10 times inference on image: ./images/ski.jpg at 1.5624 seconds/image.’

 

可以看到,当前吞吐量每秒不足一帧(FPS),代表性能表现比较差。在这种情况下,我们实际是未使用GPU资源运行TesnorFlow图 –model cmu。很多朋友都清楚,这类模型在CPU上无法获得最佳运行性能。如果重复设置步骤,并选择使用搭载英伟达T4 GPU的g4dn.xlarge实例,则结果将大为不同:

‘[TfPoseEstimatorRun] [INFO] 10 times inference on image: ./images/ski.jpg at 0.1708 seconds/image’ 

 

结果显示5.85 FPS,性能得到了显著提升。

使用Neuron编译完成的CMU模型

到这里,我们已经使用了repo中自带的模型工件。接下来,我们换一种方式:不再使用原始下载脚本来获取CMU模型,而是将Neuron编译完成的模型复制到 ./models/graph/cmu/graph_model.pb,而后再次运行测试:

aws s3 cp s3://<MY_BUCKET_NAME>/graph_opt.pb ./models/graph/cmu/graph_model.pb

 

如果您之前已经对未经Neuron编译的模型进行过测试,请确保在notebook上重新启动Python内核。重新启动内核,能够保证关闭所有TensorFlow会话,为基准测试提供完全等效的空白环境。再次运行同一notebook,您将得到以下日志条目:

‘[TfPoseEstimatorRun] [INFO] 10 times inference on image: ./images/ski.jpg at 0.1709 seconds/image.’

 

结果显示,与g4dn.xlarge实例相比,新环境能够在降低约30%运营成本的前提下实现相同的帧速率。这意味着将工作负载转移至基于AWS Inferentia的实例虽然无法在吞吐量层面带来直观可见的性能提升,但却确实具有成本优势。例如,当迁移至AWS Inferentia时,Amazon Alexa文本到语音转换团队得以将推理成本降低达50%

我们决定剖析已编译的图版本,并进一步寻求对OpenPose管道进行端到端推理性能调优的空间。Neuron与TensorFlow相集成之后,可实现对本地配置文件库的访问。要剖析Neuron编译图,我们使用TensorFlow Python分析器,在estimator方法上检测TensorFlow会话所运行的命令

from tensorflow.core.protobuf import config_pb2
from tensorflow.python.profiler import model_analyzer, option_builder

run_options = config_pb2.RunOptions(trace_level=config_pb2.RunOptions.FULL_TRACE)
run_metadata = config_pb2.RunMetadata()

peaks, heatMat_up, pafMat_up = self.persistent_sess.run(
    [self.tensor_peaks, self.tensor_heatMat_up, self.tensor_pafMat_up], feed_dict={
        self.tensor_image: [img], self.upsample_size: upsample_size
    }, 
    options=run_options, run_metadata=run_metadata
)

options = option_builder.ProfileOptionBuilder.time_and_memory()
model_analyzer.profile(self.persistent_sess.graph, run_metadata, op_log=None, cmd='scope', options=options)
The model_analyzer.profile method prints on StdErr the time and 

 

其中model_analyzer.profile方法会在StdErr上输出TensorFlow图上各项操作所对应的时间与内存消耗。使用原始代码,Neuron操作与平滑操作占据整个图运行时间中的大部分比例。以下StdErr日志输出结果显示,总图运行时间为108.02毫秒,其中平滑操作耗费43.07毫秒:

node name | requested bytes | total execution time | accelerator execution time | cpu execution time
_TFProfRoot (--/16.86MB, --/108.02ms, --/0us, --/108.02ms)
…
   TfPoseEstimator/conv5_2_CPM_L1/weights/neuron_op_ed41d2deb8c54255 (430.01KB/430.01KB, 58.42ms/58.42ms, 0us/0us, 58.42ms/58.42ms)
…
smoothing (0B/2.89MB, 0us/43.07ms, 0us/0us, 0us/43.07ms)
   smoothing/depthwise (2.85MB/2.85MB, 43.05ms/43.05ms, 0us/0us, 43.05ms/43.05ms)
   smoothing/gauss_weight (47.50KB/47.50KB, 18us/18us, 0us/0us, 18us/18us)
…

 

其中,OpenPose中平滑方法提供针对置信度图计算得出的高斯模糊结果。通过对此项计算进行优化,我们能够从端到端姿态估计当中榨取出更好的性能。我们将estimator.py脚本上的平滑器filter参数从25修改为5。在使用新配置之后,总运行时间降低至67.44毫秒,其中平滑计算如今只需要2.37毫秒——时间节约量达37%!而在g4dn实例上,相同的优化对于运行时间几乎没有任何影响。大家还可以更改同一参数,并通过本地副本重新安装 tf-pose-estimation repo的方式优化端到端管道版本。

我们使用七种不同的实例类型与大小运行了同一项基准测试,借此评估端到端图像处理管道的性能与推理成本优化效果。为了进行直观比较,我们还纳入了Amazon EC2按需实例的计费指标

结果显示,即使是Inf1实例中最小的xlarge实例,其吞吐量也要比g4dn实例中最强劲的8xlarge实例高2倍,且处理1000张图像的成本仅为后者的十二分之一。对比最优的两个选项,inf1.xlarge和g4dn.xlarge(成本最低的GPU选项),Inf1能够将每1000张图像的处理成本较g4dn.xlarge降低72%,相当于性价比提升3.57倍。下表整理了此次基准测试的各项发现:

inf1.xlarge inf1.2xlarge inf1.6xlarge g4dn.xlarge g4dn.2xlarge g4dn.4xlarge g4dn.8xlarge
图像处理时间(秒/图像) 0.0703 0.0677 0.0656 0.1708 0.1526 0.1477 0.1427
吞吐量(FPS) 14.22 14.77 15.24 5.85 6.55 6.77 7.01
1000张图像处理时间(秒) 70.3 67.7 65.6 170.8 152.6 147.7 142.7
按需实例价格 $ 0.368 $ 0.584 $ 1.904 $ 0.526 $ 0.752 $ 1.204 $ 2.176
每1000张图像处理成本 $ 0.007 $ 0.011 $ 0.035 $ 0.025 $ 0.032 $ 0.049 $ 0.086

以下图表,总结了xlarge与2xlarge实例在处理1000张图像时的吞吐量与成本情况。

通过在端到端管道中引入数据并行处理方法,我们又进一步降低了Inf1实例的图像处理成本并提升了 tf-pose-estimation吞吐量。上表中列出的数值,与单一AWS Inferentia处理核心(即Neuron核心)的使用方式有关。基准实例中包含4个核心,因此仅使用其中1个显然会产生资源浪费。我们使用Python joblib库以比较粗糙的方式将main()函数调用实现并发扩展至四个线程当中。这种模式能够将吞吐量提升至56.88 FPS,并将每1000张图像的成本降低至0.002美元以下。这意味着更好的批处理策略可以让OpenPose在AWS Inferentia实例上的运行性价比得到更进一步的改善。

规模较大的CMU模型还能提供更好的姿态估计性能。例如,大家可以在包含多重景深与相应拍摄对象的场景下(参见下图),使用Neuron SDK编译模型进行多姿态检测。

安全关闭并进行资源清理

在Amazon EC2控制台上选择您的编译与推理实例,而后从Actions下拉菜单中选择Terminate。您编译之后的模型将被保存在s3://<MY_BUCKET_NAME> 当中以备后续重复使用。如果您对实例中的代码做出了更改,也请注意进行保存。实例终止仅会导致存储在实例主卷内的数据丢失。

总结

在本文中,我们分步完成了对OpenPose TensorFlow版开源模型的编译,更新自定义端到端图像处理管道,并体验了能够在EC2 Infi1实例之上对ML推理时间做出分析及深度优化的工具。在调优之后,Neuron编译的TensorFlow模型较现有费率最低的GPU实例实现72%的成本节约,且性能仍旧保持一致。本文中阐述的各项操作步骤,也适用于其他ML模型类型与框架。关于更多详细信息,请参阅AWS Neuron SDK GitHub repo

感兴趣的朋友可以参阅关于AWS Inferentia芯片 与Amazon EC2 Inf1实例的更多详细信息,使用Neuron SDK在AWS Inferentia上运行您自己的定制化ML管道。

 

本篇作者

Fabio Nonato de Paula

AWS公司自主计算首席解决方案架构师。他致力于推动自主与智能系统领域的大规模机器学习与AI部署工作。Fabio还热衷于实现加速计算与分布式ML的大众化进程。在工作之余,Fabio喜欢在利弗莫尔山谷的山道上骑摩托车,或者阅读ComiXology。

Haichen Li

AWS Neuron SDK团队软件开发工程师。他致力于将机器学习框架与AWS Neuron编译器和运行时系统集成起来,并以此为基础开发出能够充分发挥Inferential硬件潜能的深度学习模型。