专栏名称: 江大白
专业有趣的AI内容平台,关注后回复【算法】,获取45家大厂的《人工智能算法岗江湖武林秘籍》
目录
相关文章推荐
十点读书  ·  别对任何关系上瘾 ·  昨天  
壹读  ·  麦当劳和肯德基“互撕”,谁赢了? ·  3 天前  
51好读  ›  专栏  ›  江大白

AI产品工程化中,如何提升AI算法系统的运行效率和性能?

江大白  · 公众号  ·  · 2025-02-24 08:00

正文

以下文章来源于微信公众号: 爱罗AI说
作者: Aleo
链接:https://mp.weixin.qq.com/s/zyVHrXcmfy-TlkumocppTw
本文仅用于学术分享,如有侵权,请联系后台作删文处理

导读
随着行业对算法工程化的需求提升,不仅要关注模型精度,还需优化运行效率。 本篇文章将探讨如何通过推理引擎优化、多线程并行计算、CUDA 加速等方法,提高视觉算法的性能,为实际业务需求提供高效、低成本的解决方案。

前面的几篇文章下来,整个多路视频流下的车流量统计系统的框架就搭起来了,但是在原始功能设计的时候,期望能在已有硬件基础上,可以实时解析到十路数据,目前的代码实现下,还有点性能问题。现在的视频帧率是20 FPS,如果是正常做项目,稍微卡一下视频帧率,微调下跟踪算法,事情就过去了,但是既然是写文章,正好借这个机会,跟大家聊一聊,视觉类的产品,如果遇到类似的产品运行性能不及预期,我们该怎么做。

这也是这个系列的第六篇内容,该系列的文章列表:
  1. 第一篇: “在实战中学AI-车流量统计项目-开篇”

  2. 第二篇: “在实战中学AI-车流量统计项目-YOLO11车辆检测-YOLO11模型的部署”

  3. 第三篇: “小白也能快速搭的多路视频推拉流服务”

  4. 第四篇: “八千字长文详解ByteTrack,tracking by detection 范式下一大力作”

  5. 第五篇: 教你快速搭建一套多路视频流实时处理系统 - 车流量统计项目框架成型

01—为什么要做算法运行性能优化

我在刚入行的时,基于深度学习的计算机视觉刚火,那时候是没有算法工程化这个岗位的,基本上都是模型优化与业务落地一把抓。但是现在随着技术和行业的发展,岗位职责分工已经做的很细了,算法的工程化变得独立且重要。
有人会说,既然这二者可以合二为一,那为什么还要分开呢,两个人的活一个人干不好吗?这根行业的发展有关,就跟以前所有人都会说我要买双鞋,然后只管好看耐穿就行,但现在你要买双鞋,还要分跑步,打篮球,踢足球,细分才能做到极致。
深度学习刚火那会虽然有人关注模型的轻量化,比如mobilenet, 模型轻量化方向的大师级别的工作,但论文中却只体现了在模型参数量和计算量上的优化,以及参数量和计算量的降低对准确率召回率的影响, 但却没有关注到底推理速度和吞吐能提升多少, 我们在实际工作中发现,其在推理速度上的提升,远低于其在参数量和计算量上降低的倍数。后面才逐步有人关注推理时延,资源消耗这些事情。
那时候,不管学术界还是工业界,都在一个野蛮生长的阶段,只要你说你是做深度学习的,那项目和融资就哐哐的进账。
记得18年那会在做车辆检验检测相关工作,其实那时候整个工程的运行效率很低,推理速度也不快,但这个技术的出现本身就是对传统作业模式的降维,大家不会认为多买几台服务器是什么问题,一个项目几百万,几万十几万的硬件成本算什么,这个项目这么炫酷,配个专门的机房客户也可以接受。
但是随着时代和技术的发展(其实就是大家卷出来的),所谓的人工智能被拉下神坛,大家更关注这东西到底能为我做什么? 客户会关心算法的性能能不能满足实际的业务需求?上了你系统的带来的受益能不能抵消购买机器的成本?
我随随便便几十路数据,你就得搞那么多台机器,上云服务这都是时刻被薅的成本,本地化部署的话,我除了买机器的钱,还得专门给你搞个机房呢,挺费劲。你这东西都得上云才能识别,时效性太慢,而且还得保障网络传输的带宽和稳定性,这东西不靠谱,能不能在端上直接搞?
客户需求的演进结合技术的发展,逐步催生出这样一个新的需求 - 如何才能将模型与算法以更低的成本,更高的性能,更快的速度运行, 这就是算法工程化存在的意义。

02—常规的优化手段有哪些

或因为我们自身对成本控制的诉求,或因为客户对时效性的诉求、部署平台的指定,我们需要对研发的视觉算法做工程化,那常规的工程化手段以及需要关注的指标有哪些呢?
首先,我们梳理下,在做算法或者模型的工程化时,我们要关注核心指标有哪些?
1. 吞吐量 ,是指单位时间内模型能推理的最大图片数,或者算法能处理的最大图片帧数。这里面还要关注,模型的单批次推理时延,资源消耗等等。
2. GPU/CPU利用率 ,关注这两个利用率可以知道,目前的硬件性能有没有被充分利用,如果硬件资源还有剩余,系统的运行性能不达标的情况下,那样有一套的优化策略;如果硬件资源没有了,系统运行性能不达标,那就会比较麻烦些,但对应也有一套优化策略。这里有几个事情需要注意:
GPU利用率(即nvidia-smi中Volatile GPU-Util 显示的值)的官方解释是,在过去采样周期内,GPU上一个或多个kernel执行的时间百分比,即它只表明这段时间内是否有kernel在执行,但无法表明资源利用的是否充分,极端情况下,只对GPU显存进行读写,也可以获得100%的GPU利用率。
一般在工程化层面做的事情都是想办法让GPU忙起来,但是是否满负荷,还得靠底层模型的资源利用效率,这不是今天聊的重点。GPU还有一个 MFU参数(即模型FLOPS利用率),是评估GPU在执行深度学习任务时计算资源利用效率的一个关键指标,后续有需求的话,可以理一理。
除了GPU或者CPU的性能利用,硬件带宽也是影响系统性能的重要因素,GPU和CPU都很强劲,通讯带宽很拉胯,那也不行,典型的木桶短板原理,一块差劲都不行。
3. 精度损失 ,一般做模型部署框架的迁移,或者量化加速,都难免会导致模型的识别精度损失,这块很难避免,框架迁移层面,一般像Pytorch向TensorRT迁移还好一些,特别向一些不常用的框架迁移,问题可能就比较多了。
在量化层面,推理精度越低,如INT8这样,难免带来更大的性能精度损失。一般情况下,低精度推理的选择都是作为最后的手段,迫不得已不用,比如INT8,在一定的校准集上做量化,在一定的测试集上做精度损失验证或者阈值参数选择,经常会存在在一组测试集上调参后,模型的识别性能指标接近,但是换了一个测试集后,性能指标差异明显的问题。
这种情况下,如果模型训练和量化部署都是自己一个人做的还好,还有方式后面针对性优化,如果不是一个人做的,难免陷入无尽的扯皮,不说了,都是泪。
今天只聊运行性能怎么优化,不是说模型怎么优化,和模型识别性能有关的指标不讨论,有了上面的三点,常规的工程优化也能包得住了。知道了该关注哪些指标,下面聊聊,常规的优化手段有哪些?
1. 用c++推理代替python推理,一般情况下,模型的训练或者前期的功能验证时,可以用python,上手快,理解简单,三方工具多等好处,可以快速让我们拿到结果,但是真上工程使用,建议使用c++,运行更高效。
2. 善用多线程,利用多线程的方式,见缝插针的将CPU和GPU的利用率拉上来,具体的使用方式可能还得结合不同的业务需求,但是二者的利用率要得上来,目前的算力资源还挺贵,不说推理速度要搞多块,有算力,没榨干,也挺亏的。
3. 模型前后处理的CUDA化,这块也挺重要的,如果GPU资源富裕,将必要的操作挪到GPU上,不仅可以减轻CPU的压力,还能大大提速。但是,也不是一味的往上移,比如GPU资源吃紧,CPU资源富裕的时候,甚至要做反向操作。方法都不是死的,主打一个因地制宜。
4. 合理使用多batch推理,如果模型不大,计算资源消耗不多,且积攒batch的时间比较短,我们要使用多batch推理,最大化利用GPU资源。
5. 选择合适的推理框架,一般我们模型训练的框架都会选择Pytorch或者TensorFlow,这两个训练框架本身也可以推理,或者它们也会出对应的推理框架,如Libtorch等,但是这些框架不一定是性能最好的,我们要选择合适的推理框架进行工程推理,比如,如果选择nvidia的卡,大概率TensorRT是躲不了的,它会针对不同的硬件,做很底层的推理性能优化,将以倍数提升模型的推理速度,降低资源消耗。
6. 选择合适的推理精度,FP32,FP16,INT8等,推理精度越高,精度保持的越好,对应推理时长越长,资源消耗越多。我们要根据项目需求,选择合适的推理精度。

03—如何结合具体项目做性能优化

上面讲的比较干,可能大家看着感觉不觉明历,但是实际上也不一定有什么感受,下面通过我们在做的实际项目,跟大家聊一聊,这些方法,具体工程中该怎么用。
整个项目,最大头的资源消耗点就是检测模型的推理资源消耗了,所以我们从这个模型为切入点,看看我们的起点是什么。我们选择的是YOLO11的x尺寸模型,输入尺寸的640*640,使用Pytorch python代码推理,前后处理在CPU上,使用 FP32精度,测试出来的推理耗时是单帧44.6毫秒,其中前处理耗时5.2毫秒,模型推理耗时19.6毫秒,后处理耗时19.8毫秒。拿到这个结果,心里一紧,这效率,还十路,每路25FPS呢,单路都够呛,下面看我们如何变魔术吧。
1. 我们将推理引擎由Pytorch转为TensoRT,推理精度设置为FP16,模型前后处理算法依旧运行在CPU上,GPU上模型前向耗时降低到6.5毫秒左右,整体单帧推理耗时由44.6毫秒降低到29.3毫秒。
2. 实现方式由python改为c++,前后处理部分的代码挪到GPU上,整个模型推理部分的耗时由29.3毫秒降低到5.24毫秒,其中,GPU上模型前向耗时由6.5毫秒降低到4.4毫秒左右。其中最主要的模型后处理部分的耗时,直接由20毫秒左右,干到几乎忽略不计。
3. 更改为多batch推理,batch设置为4,整个模型推理部分的耗时由5.24毫秒降低到4.11毫秒;batch设置为8,整个模型推理部分的耗时由5.24毫秒降低到3.84毫秒。
按照现有的条件,需处理十路视频,每路视频20FPS,即每秒需处理200帧,其中最耗时的检测模型推理这块,batch为8时,可以维持在4毫秒以内,即使加上其他数据操作的消耗,问题也不大,一帧5毫秒可以cover得住。
像跟踪和视频解码,以及统计车流量的功能都用到了线程池,相当于每一路数据均用到了一个线程单独处理,那样,一帧的处理耗时控制在50毫秒就行,日志追踪了下,时间上还很充裕,检测模型的性能瓶颈解决了,整体问题就不大了。
上面的优化项,除了多batch外,其余在前面都有讲过,今天把多batch部分的实现代码补上。
首先,在Pytorch模型转onnx的时候,就要设置batch参数,具体参考:
model = YOLO(model='yolo11x.pt')  # load a pretrained model (recommended for training)model.export(format="onnx", opset=16, batch=8, imgsz=640, simplify=True, save_dir='.')
onnx转engine的时候,没有额外的变化,只是使用python做推理的时候,需要做与batch图像生成以及推理后解码有关的适配工作,具体参考:
# 图片预处理部分def preprocess_batch(images, inputs, dst_width=640, dst_height=640):    batch_input = []    IMs = []    for image in images:        # image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)        # image = cv2.resize(image, (imgsz, imgsz), interpolation=cv2.INTER_LINEAR)        IM, img_pre = preprocess_warpAffine(image, dst_width, dst_height)        image_input = ToTensor()(img_pre)[None].cpu().numpy()        batch_input.append(image_input.astype(np.float32))        IMs.append(IM)    batch_input = np.concatenate(batch_input, axis=0)    # print(inputs[0]["shape"])    inputs[0].host = batch_input.ravel()    return IMs
    #模型推理部分def do_inference_v2(context, bindings, inputs, outputs, stream, input_tensor):    # print(len(inputs))    # Transfer input data to the GPU.    [cuda.memcpy_htod_async(inp.device, inp.host, stream) for inp in inputs]    # Run inference.    context.execute_async_v2(bindings=bindings, stream_handle=stream.handle)    # Transfer predictions back from the GPU.    [cuda.memcpy_dtoh_async(out.host, out.device, stream) for out in outputs]    # Synchronize the stream    stream.synchronize()    # Return only the host outputs.    return [out.host for out in outputs]
def inference_batch(engine, context, inputs, outputs, bindings, stream, images, batch_size):    start_time = time.time()    IMs = preprocess_batch(images,inputs)    end_time = time.time()    print("s1 "+str(end_time - start_time))    # print(IM)    start_time = time.time()       # for i in range(1000):       outs = do_inference_v2(context, bindings=bindings, inputs=inputs, outputs=outputs, stream=stream, input_tensor=inputs)    end_time = time.time()    print("s2 "+str(end_time - start_time))    results = []    # print(outs[0].size)    start_time = time.time()    for i in range(batch_size):        # print(outs[3][i:i+1].shape)        result = postprocess([outs[0][i*84*8400:(i+1)*84*8400]], IMs[i], conf_thres=0.25, iou_thres=0.45)        results.append(result)    end_time = time.time()    print("s3 "+str(end_time - start_time))    return results
c++推理的时候,需要做响应的适配,主要是与batch图像预处理以及模型推理后的数据解析有关,具体参考:
# 数据前处理部分,添加batch图像的预处理cuda代码,只写改变部分,其余参考前面文章void preprocess_batch(const std::vector<:mat>& imgBatch, float* dstDevData, const int dstHeight, const int dstWidth, cudaStream_t stream){    if(imgBatch.size() == 0){        return;    }    int dstElements = dstHeight * dstWidth * 3;    for(int i=0;i        int srcHeight = imgBatch[i].rows;        int srcWidth = imgBatch[i].cols;        int srcElements = srcHeight * srcWidth * 3;
        // middle image data on device ( for bilinear resize )        uchar* midDevData;        cudaMalloc((void**)&midDevData, sizeof(uchar) * dstElements);        // source images data on device        uchar* srcDevData;        cudaMalloc((void**)&srcDevData, sizeof(uchar) * srcElements);        cudaMemcpyAsync(srcDevData, imgBatch[i].data, sizeof(uchar) * srcElements, cudaMemcpyHostToDevice, stream);
        // calculate width and height after resize        int w, h, x, y;        float r_w = dstWidth / (srcWidth * 1.0);        float r_h = dstHeight / (srcHeight * 1.0);        if (r_h > r_w) {            w = dstWidth;            h = r_w * srcHeight;            x = 0;            y = (dstHeight - h) / 2;        }        else {            w = r_h * srcWidth;            h = dstHeight;            x = (dstWidth - w) / 2;            y = 0;        }        dim3 blockSize(32, 32);        dim3 gridSize((dstWidth + blockSize.x - 1) / blockSize.x, (dstHeight + blockSize.y - 1) / blockSize.y);






请到「今天看啥」查看全文