简介斯坦福大学教授、Tcl语言发明人JohnOusterhout曾写过一本书《软件设计的哲学》,系统地讨论了软件设计的一般原则和方法论。其核心思想是:软件设计的核心是降低复杂性。事实上,这种观点也适用于涉及底层硬件适配的软件设计。以视觉模型的开发为例。以往,在视觉模型的开发过程中,人们普遍更注重模型本身的优化,以提高速度和效果。至于图像的前处理(pre-processing)和后处理阶段,人们关注的很少。当模型计算的效率,即模型训练和推理的主要阶段越来越高时,图像的前处理和后处理阶段越来越成为图像处理任务的性能瓶颈。具体来说,在传统的图像处理过程中,前处理和后处理部分通常由CPU来操作,这将导致整个过程中超过50%到90%的工作量与前处理和后处理相关。后处理,所以它们会成为整个算法过程的性能瓶颈。1、主流CV库的局限性以上问题是目前市面上主流CV库在应用场景上的主要局限性,即对底层硬件的依赖不一致导致复杂度和性能瓶颈。正如JohnOusterhout总结复杂性的原因:复杂性来自依赖性。主流的图像处理库OpenCV有着广泛的应用场景,但在实际使用中也面临着一些问题。例如,当使用OpenCV的CPU版本进行训练和推理时,在推理阶段可能需要更高性能的版本。因为在训练场景中,可以在时间上覆盖预处理和模型推理,从而覆盖预处理的时间。但在推理流水线中,模型只包含前向推理,经过TensorRT加速后耗时大幅减少。这时候预处理的时间消耗会很高,很难被模型推理覆盖。为了减少推理场景的耗时,提高推理场景的性能,一般使用OpenCV的GPU版本进行加速。但是OpenCV的CPU和GPU版本之间的结果可能不一致。一个典型的例子就是resize算子,其计算CPU版本和GPU版本差异的方法不一致。OpenCV在训练和推理过程中使用不同版本的运算符。CPU一般用于训练,因为CPU算子覆盖率高,GPU一般用于推理,性能较好。因此,这也会导致结果对齐出现问题。也就是说,当CPU用于模型训练,GPU用于模型推理时,最终输出的结果是不会对齐的。其次,一些GPU算子的性能会下降。在OpenCV中,一些GPU算子本身就占用了大量的时间,导致整个算子的性能出现回落,甚至比CPU版本还差。第三,OpenCV的GPU算子覆盖范围有限,部分算子只有CPU版本。还有一些GPU算子对参数和数据类型的覆盖率没有CPU版高,带来了使用上的限制。最后,如果CPU算子和GPU算子在使用过程中交互使用,会带来CPU和GPU之间大量的数据拷贝和同步操作,导致整体加速性能不足。另一个常用的图像处理库是TorchVision。TorchVision在做模型推理时,部分算子缺少C++接口,调用时缺乏灵活性。如果要生成C++版本,必须通过TorchScript生成。这会在使用上造成很多不便,因为在交互使用的过程中插入其他库的算子会带来额外的开销和工作量。TorchVision的另一个缺点是运营商的覆盖率不高。以上是目前主流CV库的局限性。2.统一的CVpipeline由于前处理和后处理的性能瓶颈主要在于使用CPU计算,因此在模型计算阶段使用GPU的技术已经越来越成熟。那么,一个很自然的解决方案就是使用GPU来加速前处理和后处理,这将大大提高整个算法流水线的性能。为此,英伟达和字节跳动开源了图像预处理算子库CV-CUDA。CV-CUDA可以在GPU上高效运行,算子速度可以达到OpenCV的100倍左右。2023年1月15日9:30-11:30,由NVIDIA主办的“CV-CUDA第一期公开课”,邀请了来自NVIDIA、字节跳动、新浪微博的3位技术大咖(张毅、盛一瑶、庞峰)为深入分享相关话题,本文总结了三位专家演讲的精华。用GPU代替CPU有很多好处。首先,将前处理算子和后处理算子迁移到GPU后,可以提高算子的计算效率。其次,由于所有过程都在GPU上进行,可以减少CPU和GPU之间的数据拷贝。最后,将CPU负载迁移到GPU后,可以降低CPU负载,让CPU可以用来处理其他需要复杂逻辑的任务。将整个流程迁移到GPU后,整个流水线可以提升近30倍,从而节省计算开销,降低运营成本。从图中的数据对比可以看出,在相同的服务器和参数配置下,对于30fps的1080p视频流,OpenCV最多可以开启2-3个并行流,PyTorch(CPU)最多可以开启1.5个并行流,而CV-CUDA最多可以开启60个并行流。可以看出整体性能有了很大的提升。涉及的前处理算子包括resize、padding、normalize等,后处理算子包括crop、resize、compose等。3.异步为什么GPU可以适应前处理和后处理的加速要求?受益于模型计算与前后处理的异步性,适应GPU的并行计算能力。我们分别说明模型训练和模型推理的预处理异步。1.模型训练的预处理异步模型训练可以分为两部分,第一是数据准备,第二是模型计算。目前主流的机器学习框架,如PyTorch、TensorFlow,数据准备和模型计算都是异步的。以PyTorch为例,它会启动多个子进程进行数据准备。如图所示,包括模型计算和数据准备两个状态。两者之间存在时序关系。例如,当D0完成后,就可以进行B0,等等。从性能的角度来看,我们希望数据准备的速度跟上模型计算的速度。但在实际情况中,一些数据读取和数据预处理过程耗时较长,导致相应模型计算前存在一定的窗口期,导致GPU利用率下降。数据准备可以分为数据读取和数据预处理。这两个阶段可以串行或并行执行。比如在PyTorch的框架下串行执行。影响数据读取的性能因素很多,如数据存储介质、存储格式、并行度、执行进程数等。相比之下,数据预处理的性能因素就比较简单了,就是并行度。并行度越高,数据预处理的性能越好。也就是说,让数据预处理和模型计算异步化,增加数据预处理的并行度,可以提高数据预处理的性能。2.模型推理的异步预处理在模型推理阶段,其性能有两个指标,第一个是吞吐量,第二个是延迟。在某种程度上,这两个指标是相互排斥的。对于单次查询,服务端收到数据后,会对数据进行预处理,然后进行模型推理。所以对于单个查询来说,在一定程度上是一个串行的过程。但是这样做效率很低,会浪费大量的计算资源。为了提高吞吐量,许多推理引擎使用与训练阶段相同的策略来异步准备数据和模型计算。在数据准备阶段,会积累一定数量的query,合并成一个batch,然后进行后续的计算,以提高整体的吞吐量。在吞吐量方面,模型推理和模型训练是相似的。将数据预处理阶段从CPU转移到GPU可以提高吞吐量。同时,从延迟的角度来看,对于每条查询语句,如果能够减少预处理过程所花费的时间,那么每条查询的延迟也会相应缩短。模型推理的另一个特点是模型的计算量比较小,因为只涉及前向计算,不涉及后向计算。这意味着模型推理对数据预处理有更高的要求。3、核心问题:CPU资源竞争假设有足够的CPU资源进行计算,理论上预处理不会成为性能瓶颈。因为一旦发现性能跟不上,只需要加进程做预处理操作即可。因此,只有当存在对CPU资源的竞争时,数据预处理才可能成为性能瓶颈。在实际业务中,CPU资源竞争的情况非常普遍,这会导致后续训练和推理阶段的GPU利用率较低,从而降低训练速度。随着GPU计算能力的不断提升,可以预见数据准备阶段的速度要求会越来越高。为此,将预处理部分移至GPU,缓解CPU资源竞争问题,提高GPU的利用率,是自然而然的选择。总体来说,这种设计降低了系统的复杂度,将模型流水线的主体直接适配到GPU上,对于提高GPU和CPU的利用率有很大的帮助。同时也避免了不同版本之间的结果对齐问题,减少了依赖,符合JohnOusterhout提出的软件设计原则。4.CV-CUDA将预处理和后处理过程放在GPU上,需要满足多种条件。首先是它的性能至少比CPU好。这主要是基于GPU的高并发计算能力。二是预处理的加速,不能对模型推理等其他过程造成负面影响。对于第二个需求,CV-CUDA的每个算子都有一个stream和CUDA显存接口,这样可以更合理的分配GPU资源,让这些预处理算子在GPU上运行的时候,不会对GPU造成太大的影响模型计算本身。第三,互联网公司的业务需求非常多样化,涉及的模型种类多,相应的预处理逻辑也多。因此,需要将预处理算子开发成定制化的算子,以具有更大的灵活性来实现复杂的逻辑。总体而言,CV-CUDA从硬件、软件、算法、语言等方面加速了模型流水线中的前处理和后处理阶段,统一了整个流水线。1、硬件在硬件方面,CV-CUDA基于GPU的并行计算能力,可以大大提高前处理和后处理的速度和吞吐量,减少模型计算的等待时间,提高利用率的GPU。CV-CUDA支持批处理和可变形状模式。Batch模式支持批处理,可以充分发挥GPU的并行特性,而OpenCV无论CPU还是GPU版本,都只能调用单张图像。VariableShape模式是指在一个batch中,每张图片的长宽可以不同。网上的图片一般都是长宽不一致的。主流的框架是将长宽resize到一样大小,然后把长宽一样的图片打包成一个batch,然后对batch进行处理。CV-CUDA可以直接批量处理不同长宽的图像,不仅提高了效率,而且使用起来也非常方便。VariableShape的另一个含义是,在处理图像时,可以为每张图像指定某些参数,比如rotate,可以为一批图像指定每张图像的旋转角度。2.在软件和软件方面,CV-CUDA开发了大量的软件优化方法进行进一步优化,包括性能优化(如内存访问优化)和资源利用优化(如显存预分配),因此它可以在训练和推理场景中高效地运行在云端。首先是内存预分配设置。OpenCV调用GPU版本时,部分算子会在内部执行cudaMalloc,导致耗时大幅增加。在CV-CUDA中,所有的显存预分配都在初始化阶段进行,但在训练和推理阶段,不进行显存分配操作,从而提高了效率。其次,所有操作符都是异步操作的。CV-CUDA集成了大量的内核,从而减少了内核的数量,从而减少了内核的启动时间和数据的拷贝擦除,提高了整体的运行效率。第三,CV-CUDA还对内存访问进行了优化,如联合内存访问、向量化读写等,以提高带宽利用率,并使用共享内存来提高内存访问和读写效率。最后,CV-CUDA在计算上也做了很多优化,比如fastmath,warpreduce/blockreduce等。3.算法在算法方面,CV-CUDA算子是自主设计和定制的,可以支持非常多的复杂的逻辑实现,易于使用和调试。如何理解独立设计?图像处理库中的算子调用有两种形式,一种是整体流水线形式,只能获取流水线的结果,如DALI,另一种是模块化独立算子形式,可以获取每个算子A单独的结果,例如OpenCV。CV-CUDA采用与OpenCV相同的调用形式,使用和调试更加方便。4.在语言方面,CV-CUDA支持丰富的API,可以将前处理和后处理无缝连接到训练和推理场景。这些API包括常用的C、C++、Python接口等,可以让我们同时支持训练和推理场景。它还支持PyTorch、TensorRT接口。未来,CV-CUDA还将支持Triton、TensorFlow、JAX等接口。在推理阶段,可以直接使用Python或C++接口进行推理,只要保证推理时前后处理、模型、GPU在一个流上即可。五、应用案例通过展示CV-CUDA在NVIDIA、字节跳动、新浪微博的应用案例,我们可以体会到CV-CUDA带来的性能提升有多么显着。首先是英伟达展示的图像分类案例。在图片分类流水线中,首先是JPEGdecode,对图片进行解码;绿色部分是预处理步骤,包括resize、convertdatatype、normalize和reformat;蓝色部分是使用PyTorch的前向推理过程,最后对分类结果进行评分排序。对比CV-CUDA和OpenCV的CPU版本和GPU版本的性能,可以发现GPU版本的OpenCV比CPU版本可以获得更大的性能提升,通过应用CV-CUDA,性能可以翻倍。比如OpenCV的CPU算子每毫秒可以处理22张图片,GPU算子每毫秒可以处理200多张图片,而CV-CUDA每毫秒可以处理500多张图片,其吞吐量是OpenCV的CPU的20多倍是GPU版的两倍,性能提升明显。接下来是字节跳动展示的OCR1、OCR2、视频多模态三个案例。在模型训练方面,可以看到在OCR1、OCR2、视频多模态这三个任务上,使用CV-CUDA后都获得了50%到100%的性能提升。为什么会有这么大的性能提升?其实这三个任务的共同点之一就是它们的图像预处理逻辑都非常复杂,比如decode、resize、crop等等,而且这些还是大类。其实每个算子类别里面可能还有很多小的。类或子类预处理。这三个任务,在预处理环节可能会有十几种数据增强,所以对CPU的计算压力非常大。如果能把这部分计算移到GPU上,CPU的Resource竞争会明显减少,整体吞吐量会大大提高。最后是新浪微博展示的视频处理案例。对于视频处理过程,传统的做法是先在CPU环境下对视频帧进行解码,将原始字节流解码为图像数据,然后进行一些常规操作,如resize、crop等,最后上传数据到GPU进行处理。具体模型计算。CV-CUDA的处理方式是将CPU解码后放在内存中的字节流上传到GPU,预处理也位于GPU上,使其与模型计算无缝对接,不需要在显存和显存之间传输。复制操作。图中显示了OpenCV(奇数)和CV-CUDA(偶数)的处理时间。蓝色是模型的消耗时间,橙色是解码的消耗时间,绿色是预处理的消耗时间。.OpenCV分为CPU解码和GPU解码两种模式,而CV-CUDA只采用GPU解码模式。可以看出,对于CPU解码的OpenCV,OpenCV的解码和预处理要比CV-CUDA耗时多。再看OpenCV对GPU解码的使用,可以看出OpenCV和CV-CUDA在模型和解码部分的耗时很接近,但是在预处理方面还是有很大的差距。在整体流水线对比方面,CV-CUDA也有着明显的优势。一方面,CV-CUDA更节省CPU资源,即在充分利用GPU利用率的情况下,CV-CUDA只需要OpenCV10%的CPU配置;,CV-CUDA也节省了更多的GPU资源。在整体流水线中,CV-CUDA的效率提升了70%。六、未来展望CV-CUDA可以有效解决模型训练和推理阶段的CPU资源竞争问题,从而提高模型训练和推理的效率。但是如何正确认识CV-CUDA的优势呢?需要了解其功能的基本前提,相对于CPU和OpenCV,其优势并不是绝对的。首先,CV-CUDA并不是万灵药。比如在模型训练阶段,如果瓶颈不在预处理,而在数据读取和模型推理。这时候如果用CV-CUDA来代替原来的预处理方案,其实也没什么用。另外,在使用CV-CUDA的过程中,如果将CPU和GPU的工作负载合理分配给预处理逻辑,有时可以取得更好的性能。比如CPU还是可以对图片进行decode和resize,resize之后再放到GPU上处理。为什么要把解码和调整大小放在CPU上?首先,对于图像解码,GPU的硬件解码单元其实是有限的。其次,对于resize,一般情况下,resize会将比较大的图片变成比较小的图片。如果在调整大小之前将数据复制到GPU,可能会占用大量显存数据处理带宽。当然,CPU和GPU之间如何分配工作负载,需要根据实际情况判断。最重要的原则是不要在CPU和GPU之间交错计算,因为跨设备传输数据会产生开销。如果交替过于频繁,可能会抵消计算本身带来的好处,导致性能非但没有提高,反而有所下降。2022年12月,CV-CUDA发布alpha版,包含常用的Flip、Rotate、Perspective、Resize等20多个算子,目前OpenCV的算子更多,有数千个算子。CV-CUDA目前只对比较常用的算子进行加速,未来会增加新的算子。今年3月,CV-CUDA将发布测试版,新增20多个算子,达到50多个算子。beta版会包含一些非常有用的算子,比如ConvexHull、FindContours等。7.结语回顾一下CV-CUDA的设计,我们可以发现其背后并没有什么复杂的原理,甚至可以说是一目了然。从复杂度来看,这可以说是CV-CUDA的优势所在。《软件设计的哲学》提到了一个判断软件复杂度的原则——如果一个软件系统难以理解和修改,那么它就很复杂;如果容易理解和修改,那就很简单了。CV-CUDA的有效性可以理解为模型计算阶段对GPU的适应性,带动前处理和后处理阶段对GPU的适应性。而这种趋势才刚刚开始。
