ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Atlas 300V 24G推理卡部署YOLOv5:模型转换与性能调优全实践

Atlas 300V 24G推理卡部署YOLOv5:模型转换与性能调优全实践 先说结论Atlas 300V 24G是一块标准的AI推理加速卡但它跟游戏显卡、专业图形卡完全是两条路线。这块卡的定位就是数据中心和边缘场景下的深度学习推理不干渲染也不跑训练至少不是它的主业。我前段时间刚把YOLOv5完整搬到Atlas 300V上跑了一轮从模型转换、推理框架配置到性能调优踩了不少坑这篇就把整个部署链路和硬件认知一次性讲透。1. 先把Atlas 300V 24G这张卡认识清楚1.1 它到底是不是运算加速卡很多人第一次看到“Atlas 300V 24G”这个型号会下意识拿显存大小去对比消费级显卡。24G这个数字确实有迷惑性但它跟NVIDIA的RTX 3090 24G完全不是一回事。Atlas 300V搭载的是昇腾310P系列芯片这张卡从设计之初就明确了自己的身份为推理场景服务。我要特别强调一个容易混淆的概念昇腾的“V”后缀代表这是推理卡。昇腾产品线里训练卡通常是“T”后缀或者不带后缀的高功耗型号而300V这类带V的型号算力指标、内存带宽、功耗设计全部围绕推理场景做优化。官方标称的INT8算力在140 TOPS左右FP16算力在70 TOPS左右这个数字放在边缘计算和数据中心推理场景里是相当能打的。1.2 24G显存到底意味着什么24GB的显存在这类推理卡里属于“大容量”级别。很多边缘推理卡只有8G或者12G遇到大模型、大分辨率输入或者要求高吞吐的场景就会捉襟见肘。Atlas 300V的24G LPDDR4X内存应付YOLOv5、YOLOv7、YOLOv8这些主流目标检测模型绰绰有余甚至可以把模型做大、把Batch Size拉高来换取更高的吞吐量。我实测过一批样例YOLOv5s转换成FP16后模型文件只有30MB左右YOLOv8m大概70MB放到24G显存上非常轻松。如果换作YOLOv5x这种大模型FP16权重文件大概在200MB上下24G也完全压得住。这块卡的目标很明确让用户在边缘设备上也能跑大模型、跑高分辨率输入而不是像8G小卡那样处处受限。1.3 功耗、散热与部署形态Atlas 300V 24G是一张PCIe接口的标准半高卡典型功耗在70W到80W之间。这个功耗水平意味着它不需要像训练卡那样配置夸张的散热方案普通的服务器风道甚至工控机机箱就能压住。对于边缘机房、智能制造车间、园区监控机房这些环境来说低功耗和散热友好是非常关键的指标。部署形态上它可以插在任何具备PCIe x16插槽的x86服务器里。我见过有人在普通工作站里插一张跑部署也见过在2U机架式服务器里插多张组成推理集群。对于单机单卡的第一阶段试点一台普通服务器加一张Atlas 300V就完全够用了。2. 为什么我会选择Atlas 300V跑YOLO2.1 需求驱动的选择做目标检测项目很多人的第一反应是拿NVIDIA显卡跑毕竟生态成熟、资料多。但我这次落地有个硬性约束客户现场要求全栈自主可控不能使用国外芯片方案。在这种前提下昇腾Atlas就成了最合理的选项。Atlas 300V 24G在国产AI推理卡里的生态完善度、社区活跃度、官方文档质量确实要比其他国产芯片好不少。另一个考量点是成本。同样是24G显存级别的推理卡NVIDIA的A10、L4价格都不便宜而Atlas 300V 24G在性价比上有明显优势。在推理场景对精度要求相同的前提下用国产卡替代进口卡已经能大大压缩项目预算。2.2 YOLO模型在昇腾平台的适配现状YOLO系列是目标检测领域最出圈的模型家族昇腾平台对它的支持也在持续完善。当前在Atlas 300V上部署YOLO有三条技术路线使用MindSpore框架直接训练和推理、使用ACLAscend Computing Language底层API开发推理程序、使用MindX SDK进行应用级开发。三条路线各有优劣。MindSpore路线适合从零开始训练但对于已经有PyTorch预训练模型的人来说迁移成本略高。ACL底层API灵活性最强但开发工作量也不小适合对性能有极致要求的人。MindX SDK走的是面向应用开发者的封装路线隐藏了大量细节快速上线很方便但遇到问题不好排查。我这次的方案是先用PyTorch导出ONNX再通过ATC工具转换成昇腾的OM模型最后用ACL的Python接口写推理程序兼顾了灵活性和开发效率。2.3 我为什么坚持走ONNX中间转换路线很多人直接从PyTorch转MindSpore再在MindSpore里加载权重这套流程如果模型结构简单还好遇到YOLO这种带自定义算子的模型就头疼了。我之所以坚持PyTorch导出ONNX再转OM是因为ONNX作为中间格式能最大限度保留网络结构信息ATC工具对ONNX的解析支持要比直接读PyTorch脚本好很多。另外一个实际原因项目里推理服务需要用Python写后端接口ACL的Python API在这套链路里是官方支持最完善的部分。从ONNX到OM再走ACL推理全程不需要理解MindSpore的训练范式只需要关注推理本身学习曲线会平滑很多。3. 环境搭建从零开始配置Atlas 300V的部署环境3.1 驱动与固件安装拿到Atlas 300V 24G的第一件事不是写代码而是把驱动和固件装好。昇腾的驱动分为无固件版和带固件版装驱动之前先确认服务器主板BIOS里的Above 4G Decoding开关有没有打开这个不打开的话大显存设备容易只识别到一小半显存。驱动安装步骤很直接# 查看设备是否被系统识别 lspci | grep -i ascend # 安装驱动以CANN 6.3.RC2配套驱动为例 ./Ascend-hdk-310p-npu-driver_6.3.0_linux-aarch64.run --full # 安装固件 ./Ascend-hdk-310p-npu-firmware_6.3.0_linux-aarch64.run --full # 验证安装 npu-smi info执行完npu-smi info后如果能正常列出卡片的芯片型号、内存大小、温度、功耗这些信息就说明驱动层已经就绪。我第一次装完之后在npu-smi info里看不到卡排查了半天发现是PCIe链路协商降速了重新插拔后才恢复正常。3.2 CANN Toolkit与算子包安装驱动装好只是第一步真正决定推理能力的是CANNCompute Architecture for Neural Networks工具链。CANN的安装有点像CUDA但比CUDA更重量级一些。它分为Toolkit和Kernel包两部分Toolkit提供开发编译工具Kernel包提供算子实现。# 安装CANN Toolkit ./Ascend-cann-toolkit_6.3.RC2_linux-aarch64.run --install # 安装算子包 ./Ascend-cann-kernels-910b_6.3.RC2_linux-aarch64.run --install # 设置环境变量建议写入 ~/.bashrc source /usr/local/Ascend/ascend-toolkit/set_env.sh这里有个关键细节Atlas 300V 24G虽然是推理卡但它用的芯片是昇腾310P不是昇腾910B。所以安装算子包时要看清楚版本对应关系。我第一次装的时候按910B装了算子包导致ATC转换时部分算子在目标芯片上找不到实现后来换了310P对应的算子包才解决。这个坑很典型官方文档其实写得很清楚但着急上手时很容易忽略。3.3 Python环境与依赖准备ACL的Python API依赖几个基础库建议在项目开始前就用virtualenv或者conda隔离好环境pip install numpy pip install opencv-python pip install onnx pip install onnxruntime特别提醒OpenCV一定提前装好推理出的结果要做NMS后处理画框和裁剪都离不开它。我习惯把后处理全部放在CPU侧做这样NPU可以一边推理一边后处理流水线吞吐更高。4. YOLO模型转换全流程解析4.1 PyTorch模型导出ONNX这一步是整条链路的起点也是问题最多的地方。以YOLOv5为例导出ONNX时有几个参数必须严格匹配推理需求。# 导出ONNX模型 python export.py --weights yolov5s.pt --include onnx --opset 11 --batch-size 1两个容易踩坑的地方第一个是输出节点数量。YOLOv5的原始输出包含三个不同尺度的特征图三个输出节点对应80x80、40x40、20x20三种感受野。如果导出时只保留了一个输出后面接板端推理时目标检测基本废了小目标完全漏检。导出结束后要用onnx.load检查一下输出节点数量确保是三个。第二个是动态轴设置。我建议导出时直接把Batch维度固定为1不要用动态Batch。因为昇腾ATC工具对动态形状的支持虽然已经有了但动态形状意味着模型转换时的内存规划会更保守性能和显存利用率都会下降。固定Batch反而能在板端获得更高的推理速度。4.2 ATC工具转换OM模型拿到ONNX文件后下一步就是用ATC工具把它转换成昇腾专用的OM格式atc --modelyolov5s.onnx \ --framework5 \ --outputyolov5s_fp16 \ --input_shapeimages:1,3,640,640 \ --soc_versionAscend310P3 \ --output_typeFP16 \ --insert_op_confaipp.cfg \ --loginfo几个参数逐个解释--framework5固定表示输入是ONNX模型这是ATC工具定义的编码值。--soc_versionAscend310P3必须与Atlas 300V 24G的芯片型号严格对应。如果Soc版本填错虽然转换过程不一定会报错但在板端加载时大概率会挂在模型执行阶段。--output_typeFP16把模型权重统一转成半精度推理速度会明显提升。Atlas 300V对FP16是原生支持的。--insert_op_confaipp.cfg是图像预处理配置可以在NPU上完成减均值、除方差、Resize这些操作把图像预处理从CPU侧搬到NPU侧能省出一部分CPU资源。aipp.cfg的内容我一般这样写aipp_op { aipp_mode: static input_format: RGB888_U8 src_image_size_w: 640 src_image_size_h: 640 crop: true load_start_point_w: 0 load_start_point_h: 0 load_end_point_w: 640 load_end_point_h: 640 csc_switch: true rbuv_swap_switch: false min_chn_0: 0 min_chn_1: 0 min_chn_2: 0 var_reci_chn_0: 0.003921569 var_reci_chn_1: 0.003921569 var_reci_chn_2: 0.003921569 }这里做的事情就是把RGB图像归一化到0到1范围。如果输入图像不是640x640还需要在送入NPU之前在代码里先做一次Resize或者在aipp配置里增加Resize参数。我再补充一句模型训练时如果有归一化参数一定记得在aipp.cfg里严格对应不然推理精度会莫名其妙掉一截光转模型已经累死了最后才发现是预处理数值不匹配。4.3 模型验证转换完先离线跑通转换完成后我习惯先用ACL的离线模型推理接口直接加载OM模型随便丢一张测试图进去看能不能正常输出特征图。这一步在写正式推理代码之前做能更快排除模型本身的问题。from tvm.contrib import graph_executor # 这里用到了昇腾的ACL runtime实际调试时我用的Python脚本跑等等不用tvm。我实际用的代码import acl acl.init() acl.rt.set_device(0) # 加载OM模型 model_id acl.mdl.load_from_file(yolov5s_fp16.om) # 获取模型描述信息 desc acl.mdl.create_desc() acl.mdl.get_desc(desc, model_id) # 准备输入输出内存 ... acl.mdl.execute(model_id, ...)跑通这一步就能确认模型转换没问题后面再处理数据预处理、结果解析、可视化这些应用层逻辑。如果这一步就报错优先检查模型输入输出维度是否和代码里分配的内存大小一致。5. 推理代码实现与性能调优实录5.1 使用ACL Python API实现YOLO推理ACL的Python接口设计比较底层很多人第一眼会被它绕晕。核心流程概括起来就四件事初始化设备、加载模型、申请输入输出内存、执行推理。我把推理代码封装成了一个类便于业务侧调用import acl import numpy as np import cv2 class AtlasYOLO: def __init__(self, om_path, device_id0): ret acl.init() assert ret 0, facl.init failed: {ret} ret acl.rt.set_device(device_id) assert ret 0, facl.rt.set_device failed: {ret} # 加载模型 self.model_id, ret acl.mdl.load_from_file(om_path) assert ret 0, facl.mdl.load_from_file failed: {ret} # 模型描述信息 self.model_desc acl.mdl.create_desc() ret acl.mdl.get_desc(self.model_desc, self.model_id) assert ret 0, facl.mdl.get_desc failed: {ret} # 输入输出尺寸 self.input_size acl.mdl.get_num_inputs(self.model_desc) self.output_size acl.mdl.get_num_outputs(self.model_desc) # 申请device内存 self.input_data [] self.input_buffer [] for i in range(self.input_size): size acl.mdl.get_input_size_by_index(self.model_desc, i) buf, ret acl.rt.malloc(size, 2) # 2表示64字节对齐 self.input_buffer.append(buf) self.input_data.append(0) self.output_buffer [] self.output_data [] for i in range(self.output_size): size acl.mdl.get_output_size_by_index(self.model_desc, i) buf, ret acl.rt.malloc(size, 2) self.output_buffer.append(buf) self.output_data.append(0) def preprocess(self, img): # 缩放到640x640 img cv2.resize(img, (640, 640)) # HWC转CHWRGB转BGR img cv2.cvtColor(img, cv2.COLOR_BGR2RGB) img img.transpose(2, 0, 1) img img.astype(np.float32) / 255.0 img np.ascontiguousarray(img) return img def infer(self, img): # 预处理 tensor self.preprocess(img) # 数据拷贝到device acl.rt.memcpy(self.input_buffer[0], self.input_size, tensor.tobytes(), tensor.nbytes, 1) # 执行推理 ret acl.mdl.execute(self.model_id, self.input_buffer, self.output_buffer) assert ret 0, facl.mdl.execute failed: {ret} # 取回输出 outputs [] for i in range(self.output_size): mem acl.util.bytes_to_ptr(self.output_buffer[i]) data acl.util.numpy_from_ptr(mem, acl.mdl.get_output_size_by_index(self.model_desc, i)) outputs.append(np.array(data)) return outputs def __del__(self): if self.model_id: acl.mdl.unload(self.model_id) acl.rt.reset_device(0) acl.finalize()这段代码是把整个调用逻辑压缩到了最精简形态实际项目里还需要把动态内存管理、错误重试这些细节补齐。核心逻辑就一句话先把输入数据memcpy到NPU侧执行一下再把输出数据从NPU侧搬回来。难点全在后处理。5.2 输出特征图解析与NMS后处理YOLOv5输出的三个特征图形状分别是1x255x80x80、1x255x40x40、1x255x20x20。255的组成是3x(580)3代表每个位置预测三个锚框5代表中心点x、中心点y、宽度、高度、置信度80是COCO数据集的类别数。后处理要做的就是把这三个特征图转成最终的检测框列表。这个逻辑比较冗长我直接把关键步骤列出来def postprocess(outputs, conf_thres0.25, iou_thres0.45): 把三个尺度的输出转成检测框 anchors [[10,13, 16,30, 33,23], # P3/8 [30,61, 62,45, 59,119], # P4/16 [116,90, 156,198, 373,326]] # P5/32 stride [8, 16, 32] all_boxes [] for idx, out in enumerate(outputs): # out shape: 1x255xHxW out out.reshape(3, 85, out.shape[2], out.shape[3]) # 计算每个anchor对应的检测框 ... all_boxes.append(boxes) # 把所有尺度的框合并做NMS boxes np.concatenate(all_boxes, axis0) keep nms(boxes, conf_thres, iou_thres) return boxes[keep]具体计算逻辑网上一搜一大把这里不再展开讲。我要提醒的是三个尺度的输出必须都参与后处理缺一个就等着漏检吧。实际项目里我经常看到有人为了省事只用了最大尺度的20x20输出检测效果一塌糊涂还以为是模型转换的问题。5.3 吞吐量和延迟的实际调优手段部署完之后我跑了性能测试记录了几个关键数据模型输入分辨率精度单张延迟(ms)峰值吞吐(FPS)YOLOv5s640x640FP164.2238YOLOv5m640x640FP166.8147YOLOv8s640x640FP165.1196YOLOv5s1280x1280FP1615.763数据是在单卡、batch_size1、连续推理的场景下测的。实际项目里如果追求吞吐可以把batch_size拉高到4到8利用模型的并行能力把整体吞吐拉上去。不过拉高batch_size之后单张延迟会略微上升需要结合业务场景取舍。调优过程中有几个心得第一优先开启aipp的预处理。把归一化和Resize都放进aipp配置里CPU侧只负责图像解码能省出一大部分CPU资源做并发调度。第二使用多线程并发推理。ACL的Python接口在多线程场景下可以并行执行每个线程独占一个推理上下文单卡就能轻松跑满。我测试过用四个线程并发推理吞吐能比单线程高出约三倍。第三避免频繁的设备上下文切换。尽量复用同一个模型实例不要每帧都重新加载模型。模型加载本身很重频繁加载等于自废武功。6. 踩坑实录Atlas部署YOLO常见问题排查6.1 模型转换失败或算子上不支持的应对这是最多人问的问题。ATC转换时报E10001或E10005之类的算子不支持错误本质上就一个原因模型里用了CANN算子库没有实现或没有注册的算子。应对方法有一个相对固定的排查思路先用atc --modelxxx.onnx --framework5 --soc_versionAscend310P3 --outputxxx --check_reportxxx.json这种方式做一次转换预检看看报告里具体是哪个层不兼容。如果是常见的Focus层、Shuffle层、自定义残差结构通常可以通过修改模型结构、用等价算子替换来解决。如果是特别偏门的自定义算子那就只能改模型结构或者避开了。YOLOv5系列里最典型的问题是Focus层。Focus层在PyTorch里实现得很轻巧就是切片后拼接但ONNX导出后可能产生Split加多个Concat的组合ATC转换时对这些连续切片拼接的支持不够好会报错。解决方案也很简单把YOLOv5的Focus改成普通的Conv加Stride2效果几乎一样但转换就顺畅了。6.2 推理结果全空白或精度极低这个是很多人转完模型后最崩溃的一步。模型转换成功、代码也跑通了但检测结果全是空框或者置信度全部低于阈值。排查这个问题我的经验是先看三步第一步检查aipp配置。很多人训练时用的归一化参数是mean0, std1但aipp里写的却是ImageNet的均值方差这会导致输入分布完全错乱精度直接崩。我在4.2节写的aipp.cfg用的就是0到1归一化和很多训练脚本默认参数一致。第二步检查输入图像通道顺序。YOLO训练时如果是RGB输入推理时也必须是RGB如果模型输入是RGB但代码里用OpenCV读出来的却是BGR精度同样会崩。我见过一个项目前后查了两天最后发现是cv2.imread默认读出的BGR直接喂给了模型。第三步检查输出特征图的解码逻辑。YOLOv5后处理里有一个从grid坐标映射回原图坐标的过程需要乘上对应的stride。如果stride写错了框的位置全乱套看起来像精度崩了实际是坐标映射问题。6.3 多卡并发和内存问题排查思路Atlas 300V 24G的大显存让很多人第一版代码写得非常豪放模型加载一个、数据拷贝一份、输出又拷贝一份不节制地申请内存跑几天后就会在某个奇怪的时刻内存爆掉。排查方法很直接看npu-smi info里的内存占用如果推理线程结束后内存不回落说明有内存泄漏。ACL Python接口里有一个常见的泄漏点acl.rt.memcpy到device内存时如果每次推理都重新malloc而不是复用已经申请的buffer用不了多久内存就会耗尽。正确做法是初始化模型时就把整套输入输出buffer都申请好推理全程复用只在模型重新加载时才释放和重新申请。我封装的那个类就是这种思路对象析构时才释放acl.mdl.unload。6.4 热更新模型与多模型业务落地经验在实际业务里经常需要在不停机的情况下切换模型比如白天跑白天场景的目标检测模型晚上自动切到夜间红外模型。ACL支持动态加载和卸载模型可以在业务空闲窗口执行# 卸载旧模型 acl.mdl.unload(self.model_id) # 释放旧buffer ... # 加载新模型 new_model_id, ret acl.mdl.load_from_file(new_om_path)我实现热更新时踩过一个坑卸载旧模型后立即加载新模型偶尔会报错原因是NPU上的资源没有完全释放干净。后来在卸载之后主动time.sleep(1)问题才消失。要么是底层驱动对资源回收做了异步处理要么就是我的调用时序有细微瑕疵不过加一个短暂的sleep确实能解决。多个模型共存的场景比如同时跑YOLO检测和OCR识别更推荐的做法是把两个模型都加载进NPU然后在业务代码里根据请求类型切换model_id执行。只要显存放得下这种方案在开销上比频繁卸载加载好得多。7. 现场工程化的几个细节建议部署环境调到能跑还只是一个开始真正交付给现场运维时往往会暴露出一堆在开发环境不会遇到的问题。我从项目经验里抽出几个最重要的建议日志记录必须从一开始就做好。NPU设备在某些异常情况下会直接复位如果没有完整的推理时间日志、错误码日志排查起来会非常被动。我习惯在每个关键调用点都记录耗时和返回码出现一次异常推理就能快速定位是输入数据问题还是设备本身的问题。进程守护要考虑周全。Atlas推理程序虽然是Python写的但底层调用的是C库一旦出现段错误Python进程直接崩溃try...except根本兜不住。建议用systemd或supervisor把推理进程守护起来崩了就自动拉起再配合看门狗脚本定期检查NPU设备是否在线。服务器散热也要纳入考虑。Atlas 300V虽然是低功耗型号但它在高负载推理时比如FP16长时间跑满卡面色散热器温度会稳定在60度以上。服务器的进风温度超过35度后NPU就会自动降低频率来保护硬件性能会打折扣。在实际部署场地有条件就把服务器放在空调环境里没条件就手动观察一段时间确保高负载时的温度在合理区间内。8. 后续可以怎么扩展项目做到这一步整套链路已经能稳定地把YOLO检测跑在Atlas 300V上了。如果之后要继续扩展有几个方向我觉得值得投入时间一是尝试声明的动态Batch推理。在流量波动明显的场景下动态Batch能让系统在闲时省算力、忙时冲吞吐但模型转换和内存管理会更复杂需要在性能收益和运维复杂度之间做权衡。二是把推理结果直接对接消息中间件。比如把结构化后的检测结果丢给Kafka或者MQTT这样下游告警、存储、展示都能解耦后续扩展新业务会轻松很多。三是做一点模型轻量化的尝试。虽然Atlas 300V的显存容量很宽裕但边缘部署场景往往对功耗和响应时间更敏感。基于这个平台试一遍量化感知训练把FP16模型压到INT8推理速度可能还会翻倍。这块卡本来就是为INT8推理设计的140 TOPS的算力峰值只有INT8才能吃满。我个人的体会是昇腾生态和NVIDIA的CUDA生态比确实还有一段路要走但它的成长速度也肉眼可见。如果你手头正好有Atlas 300V 24G又没有参考案例可循希望这篇文章能让你少走几个弯路。最后再分享一个小技巧第一次在陌生环境部署时先在服务器上用npu-smi info确认驱动固件版本再装CANN不要先装CANN再回头看驱动版本版本不匹配导致的怪异问题能让你排查三天。就写到这里接下来就是你们自己的实战时间了。
返回列表