GPU上创建目标检测Pipeline管道

Creating an Object Detection Pipeline for GPUs

今年3月早些时候,展示了retinanet示例,这是一个开源示例,演示了如何加快gpu目标检测管道的训练和部署。在圣何塞举行的英伟达GPU技术会议上介绍了这个项目。这篇文章讨论了这项工作的动机,对体系结构的一个高级描述,以及所采用的优化的一个简单的介绍。如果对GPUs上的目标检测还不熟悉,建议参考GPUs上的实时目标检测10分钟开始。

理论基础             

虽然有几个优秀的开源项目专注于目标检测,但觉得有几个原因需要创建和发布这个示例。             

推理性能和准确性之间的权衡。用于目标检测的深度学习模型可以大致分为两类:单级检测器(如单次激发检测器、YOLO、YOLOv2)和两级检测器(如更快的RCNN)。两级检测器首先提取区域建议(可能的目标)然后对其进行分类,而一级检测器则在所有背景和前景位置上实现密集分类。             

由于背景和可能目标之间的大类不平衡,单级检测器在检测精度上一直落后于最新水平。然而,两级网络精度的提高是以推理过程中较长的延迟为代价的。今天,目标应用程序的约束将决定哪种类型的模型最合适。在理想情况下,可以设计出一个具有高精度和高推理性能的模型。             

对端到端GPU处理的渴望。在一个典型的不使用GPU的目标检测管道中,会出现一些步骤,例如图像预处理和检测后处理。通过将这些函数移到GPU来加速这些函数是提高训练和推理整体性能的一种潜在策略。这还将有助于减少CPU和GPU内存之间代价高昂的数据传输,例如将完整的预处理输入张量移动到GPU,并将大的特征映射从GPU传递回CPU以进行边界框后处理。两者都可能是推理过程中的瓶颈。             

把英伟达深度学习库放在一起。NVIDIA提供了各种各样的软件库,每一个都解决了深度学习工作流的不同部分:DALI用于图像预处理,APEX/AMP用于简单的混合精度训练,TensorRT用于优化用于部署的训练模型,DeepStream用于创建智能视频分析应用程序。想创建一个端到端的例子,利用这些库中的每一个来演示GPU计算平台的价值,开发人员可以开始使用开箱即用,并为自己的用例进行扩展。

Architecture Details

接下来,需要决定在最初的设计中使用哪种架构。本节对本项目中使用的RetinaNet体系结构进行了高层次的概述,讨论了如何将其用于目标检测,并探讨了设计选择对检测精度和推理性能的影响。

RetinaNet

RetinaNet是FAIR研究人员在其论文《密集目标检测中的焦点损失》中提出的一种单级目标检测器。基于一个相对简单的设计,如图1所示,由以下部分组成:

特征提取主干网             

通常是ResNet的味道。用于从图像中提取语义信息进行后续检测。             

特征金字塔网络(FPN)             

允许网络通过向上采样顶层(丰富的语义信息)和从先前的特征提取层添加细节来“查看”更小、更详细的对象。提供不同维度的功能图,以便检测不同大小的对象。             

每一个特征映射被输入到两个子网络中             

分类子网对特征地图上的每个位置进行分类,为每个潜在的对象类和背景打分。             

功能映射也会贯穿box子网             

回归每个潜在对象周围边界框的坐标。

GPU上创建目标检测Pipeline管道

 Figure 1: The one-stage RetinaNet network architecture

RetinaNet还引入了焦距损失的概念,一种改进的损失函数来解决一级和两级探测器之间的精度差距。焦距损失补偿了背景前景类的不平衡,降低了简单示例(如背景)的重要性,使硬示例更重要。             

灵活性是这类网络的一个重要特点。特征提取主干可以很容易地改变,而无需对网络的其部分进行任何修改。这允许用户选择针对所考虑的任务或应用程序定制的特定性能精度权衡。提供了多个骨干网和明确的指标,以便于在此实现中进行选择。

Post Processing

在RetinaNet输出的推理过程中得到的边界框需要经过后处理才能得到最终的相关框。             

前面提到过,box子网会对边界框的坐标进行回归。实际上,特征映射上的每个位置都有一组预定义的锚定框,这些锚定框具有各种纵横比和比例。框子子网实际上输出精确的坐标,即从每个实际锚定框坐标(dx、dy、dw、dh)预测的边界框的增量。因此,后处理的第一步是执行从定位框增量到优化对象边界框的坐标转换。             

解码边界框坐标后,来自不同锚定的多个预测边界框通常聚集在同一对象周围。这就需要第二个后处理步骤,称为非最大抑制。此计算比较类似的预测边界框,并确保仅为最终输出检测选择每个对象得分最高的最相关边界框。             

这两个后处理步骤都可以并行化,并与其检测模型一起在GPU上运行,显著提高了性能。

Performance Overview

在开发了第一个实现之后,分析了对象检测管道的推理性能,看看是否达到了预期的目标。收集了几个RetinaNet模型的批处理大小1(包括前处理和后处理)的初步端到端推断延迟测量,这些模型是在具有NVIDIA T4 GPU的本地系统上收集的。             

每个模型对COCO 2017验证数据集中的图像进行推断,这些图像使用DALI调整大小并填充到1280×1280像素的固定输入大小。使用TensorRT优化Pythorch的RetinaNet模型,以便在T4上以INT8精度部署。             

使用TensorRT INT8 precision在T4上运行模型使其Tensor Core微体系结构能够提高吞吐量和降低延迟,同时仍然保持强大的检测精度。应用这些优化后,获得的最终端到端推理性能取决于所选主干,在0.31 mAP时每个图像的延迟为18ms,在0.39 mAP时为33ms。这些结果表明,可以设计出高精度的目标检测模型,并且仍然能够在低推理延迟的GPU上部署。

引擎

现在让看看使用的工具以及如何帮助优化目标检测管道。

使用RetinaNet CLI的基本概述 Basic overview of using RetinaNet CLI

此项目的代码是开源的,可以在NVIDIA GitHub页面上找到。提供了一个可安装的命令行模块,允许用户快速训练、测试(推断)和导出对象检测模型。这使很容易自己试验这个项目。             

训练RetinaNet模型就像指定用于训练/评估的主干架构(在本例中,是基于ResNet50的FPN)和数据集一样简单:

retinanet train retinanet_rn50fpn.pth --backbone ResNet50FPN

    --images /coco/images/train2017/ --annotations /coco/annotations/instances_train2017.json

    --val-images /coco/images/val2017/ --val-annotations /coco/annotations/instances_val2017.json

retinanet train命令还可用于微调另一个数据集的现有模型。微调The --fine-tune retinanet_rn50fpn.pth参数将加载预先训练的模型,去掉用于从原始数据集预测类的现有cls_heads的最后一层,并附加为新微调数据集定制的新的cls_heads:

retinanet train model_mydataset.pth 
    --fine-tune retinanet_rn50fpn.pth 
    --classes 20 --iters 10000 --val-iters 1000 --lr 0.0005 
    --resize 512 --jitter 480 640 --images /voc/JPEGImages/ 
    --annotations /voc/pascal_train2012.json --val-annotations /voc/pascal_val2012.json

训练模型后,可以使用retinanet infer命令评估其精度:

retinanet infer retinanet_rn50fpn.pth --images /coco/images/val2017/ --annotations /coco/annotations/instances_val2017.json

retinanet export命令抽象了将PyTorch retinanet模型转换为TensorRT引擎的复杂性,并用单个调用替换:

retinanet export model.pth engine.plan

默认情况下,retinanet导出将生成针对FP16精度的TensorRT引擎。但是,用户可以利用TensorRT的INT8校准算法,通过指定校准图像的路径来生成更高性能的INT8引擎:

retinanet export model.pth engine.plan --int8 --calibration-images /coco/images/val2017/

也可以使用retinanet infer来评估TensorRT引擎:

retinanet infer engine.plan --images /coco/images/val2017/ --annotations /coco/annotations/instances_val2017.json

DALI训练

NVIDIA的开源DALI项目专注于加速深度学习应用程序的预处理流程。DALI提供了一组高度优化的构建块,可以在CPU或GPU上运行,用于常用的预处理功能。支持多种数据格式,可以很容易地与流行的深度学习框架集成,允许预处理管道跨工作负载进行移植。             

首先需要标识训练/推理预处理管道使用DALI所需的一组运算符。使用这些运算符定义图形,以描述如何将数据转换为完全预处理的张量,这些张量可供模型使用。             

在用例中定义的图有一个单独的训练和推理预处理路径。推理图相当简单:从磁盘读取JPEG图像以及相应的检测边界框及其类。然后将传输到GPU,在那里解码、调整大小、规范化并填充到适当的大小。             

训练图使用稍微复杂一点的操作集。首先,对图像中的边界框进行随机的预期裁剪,丢弃任何不属于裁剪区域的检测。使用GPU仅部分解码包含在随机裁剪中的图像的相关部分。然后根据训练超参数随机调整解码图像的大小,随机水平翻转,最后进行归一化和填充。

def define_graph(self):
 
        images, bboxes, labels, img_ids = self.reader()
 
        if self.training:
            crop_begin, crop_size, bboxes, labels = self.bbox_crop(bboxes, labels)
            images = self.decode_train(images, crop_begin, crop_size)
            resize = self.rand4()
            images, attrs = self.resize_train(images, resize_longer=resize)
 
            flip = self.coin_flip()
            bboxes = self.bbox_flip(bboxes, horizontal=flip)
            images = self.img_flip(images, horizontal=flip)
 
        else:
            images = self.decode_infer(images)
            images, attrs = self.resize_infer(images)
 
        resized_images = images
        images = self.normalize(self.pad(images))
 
        return images, bboxes, labels, img_ids, attrs, resized_images

现在已经定义了一个DALI预处理管道,用一个用于训练和推理的迭代器来包装。该迭代器负责从DALI管道获取张量输出,执行任何最终转换(例如重新缩放边界框以匹配调整大小的图像),并将转换为PyTorch张量,以用于训练或推理。此设计还允许使DALI数据加载器与本地PyTorch数据加载器互换,以便于实验。             

比较了在Tesla T4上使用两个数据加载器为TensorRT INT8精度优化的ResNet18FPN骨干网RetinaNet网络模型的总体推断时间。注意到,与本机PyTorch数据加载器相比,使用带有TensorRT推断的DALI可以减少数据加载开销,从而获得更好的性能。对于批处理1,这种差异不大,但对于更大的批处理大小,在推断之前需要做更多的预处理工作,这种差异就变得很大。

Automatic Mixed Precision on Tensor Cores

利用混合精度进行深度学习训练是在Volta和Turing-gpu上实现张量核性能最大化的有效途径。在GTC SJ 2019上,宣布了来自英伟达APEX库的Pythorch内部自动混合精度功能(AMP)的更新。已经将这些工具集成到项目中,使RetinaNet模型的混合精度训练简单易用。迁移现有的FP32精度训练脚本以试验AMP的混合精度训练功能非常简单,只需要两个小的代码更改。             

使用AMP的第一个必要更改是使用给定的opt_level注册模型和优化器,范围从O0(常规的FP32训练)到O3(纯FP16训练)。这些opt_level对应于一组预定义参数,这些参数定义AMP将自动应用于模型和优化器的幕后更改。在实现中,使用了氧的opt_水平进行混合精度训练。默认情况下,这个opt_level将所有输入强制转换为FP16,将模型权重强制转换为FP16,在FP32中保留批处理规范化操作,并在FP32中维护模型权重的主副本,优化器将在optimizer.step(). 通过指定静态loss_scale损耗标度值128而不是动态损耗标度来覆盖此opt_level的损耗标度参数。

model, optimizer = amp.initialize(model, optimizer,
                                opt_level = 'O2' if mixed_precision else 'O0',
                                keep_batchnorm_fp32 = True,
                                loss_scale = 128.0,
                                verbosity = is_master)

训练脚本的第二个更改涉及向后传球。在使用Pythorch进行FP32训练时,人通常称my_loss.backward()计算用于优化步骤的渐变。然而,在计算梯度之前,需要在混合精度训练中缩放损失值,以避免任何潜在的数值问题。为此,使用amp.scale_loss函数自动执行损耗缩放,然后对新的缩放损耗scaled_loss调用.backward()。因为已经用AMP注册了优化器对象,所以仍然使用相同的optimizer.step()以更新模型权重。

with amp.scale_loss(cls_loss + box_loss, optimizer) as scaled_loss:
      scaled_loss.backward()
optimizer.step()

TensorRT

TensorRT是NVIDIA的高性能深度学习推理平台。提供了一个优化器组件来优化GPU上部署的深度学习模型,以及一个运行时来运行生产中的推理。为了优化用于TensorRT部署的RetinaNet模型,首先将核心PyTorch RetinaNet模型(不包括模型的包围框解码和NMS后处理部分)导出到ONNX,ONNX是深度学习模型的一种与框架无关的中间表示。接下来,使用TensorRT提供的ONNX解析器将ONNX模型表示形式中的结构和权重转换为TensorRT优化表示形式,称为INetworkDefinition对象。             

为了获得最佳性能,希望将推理管道的包围框解码和NMS步骤作为单个TensorRT INetworkDefinition对象的一部分。但是,这两个函数不容易在ONNX中表示,并且像网络的其部分一样导入TensorRT。为了使这个工作,利用TensorRT的插件层API来定义自己的自定义层,用于边界框解码和NMS。在编写优化的CUDA内核以在GPU上运行这些函数之后,使用这些内核来定义TensorRT IPluginV2对象。这个IPluginV2对象包含了TensorRT将自定义函数集成到INetworkDefinition的其余部分所需的所有信息,就好像是本机TensorRT层类型一样。

class DecodePlugin : public IPluginV2 {
void configureWithFormat(const Dims* inputDims, …) override;
int enqueue(int batchSize, const void *const *inputs, …) override;
void serialize(void *buffer, …) const override;
}
 
class DecodePluginCreator : public IPluginCreator {
IPluginV2 *createPlugin (const char *name, …) override; 
IPluginV2 *deserializePlugin (const char *name, …) override;
}
REGISTER_TENSORRT_PLUGIN(DecodePluginCreator);

将DecodePlugin连接到FPN的每个级别的class/bbox头对的输出,并将所有DecodePlugin输出组合到一个最终的NMSPlugin中,该NMSPlugin从输入图像中选择顶级检测。

// Parse ONNX FCN
auto parser = createParser(*network, gLogger);
parser->parse(onnx_model, onnx_size);
// Add decode plugins
for (int i = 0; i < nbBoxOutputs; i++) {
auto decodePlugin = DecodePlugin(score_thresh, top_n, anchors[i], scale);
auto layer = network->addPluginV2(inputs.data(), inputs.size(), decodePlugin);
}
// Add NMS plugin
auto nmsPlugin = NMSPlugin(nms_thresh, detections_per_im);
auto layer = network->addPluginV2(concat.data(), concat.size(), nmsPlugin);
// Build CUDA inference engine
auto engine = builder->buildCudaEngine(*network);

至此,已经成功地将整个PyTorch RetinaNet模型导入TensorRT。TensorRT开发工作流的下一步是优化INetworkDefinition以进行部署。使用单个API调用,TensorRT将其推理优化套件应用于网络,并生成TensorRT IEngine对象。此对象包含模型的完全优化表示,可以保存到文件中,然后重新加载以在GPU上执行模型的推断。

// Build engine
    cout << "Applying optimizations and building TRT CUDA engine..." << endl;
    _engine = builder->buildCudaEngine(*network);
    cout << "Writing to " << path << "..." << endl;
    auto serialized = _engine->serialize();
    ofstream file(path, ios::out | ios::binary);
    file.write(reinterpret_cast(serialized->data()), serialized->size());

指定优化的TensorRT引擎在默认情况下应该使用FP16精度。这样做是为了利用Volta和Turing GPUs中的张量核微体系结构来获得更好的推理性能。还提供了工作流程,为模型启用INT8精度,以获得更高的性能。             

与FP32和FP16精度不同,对TensorRT使用INT8精度需要额外的步骤。需要提供一个校准数据集,以便在优化过程中使用,以确定网络中每个层的FP32和INT8精度之间的适当比例因子,从而将推理精度的损失降到最低。通过对这些RetinaNet网络模型和COCO17数据集的实验确定,可以在精度损失最小的情况下校准模型以达到INT8精度。

if (int8) {
        builder->setInt8Mode(int8);
        ImageStream stream(batch, inputDims, calibration_images);
        Int8EntropyCalibrator* calib = new Int8EntropyCalibrator(stream, model_name, calibration_table);
        builder->setInt8Calibrator(calib);
    }

Conclusion

演示了如何为gpu创建对象检测管道的示例,并介绍了用于优化端到端工作流的NVIDIA库。相信,这项工作可以作为一个大纲,为开发人员寻求有效地创建和部署对象检测模型的gpu,并作为一个详细的例子,如何统一元素的NVIDIA深度学习软件堆栈到一个单一的工作流程。将继续开发这个项目,包括将模型集成到NVIDIA DeepStream应用程序中。一定要查看存储库以备将来更新。查看继续工作的后续帖子。