前阵子接手了一个工业视觉项目,需求很直接:在产线上跑 YOLOv5 做目标检测,推理卡已经定好了,是华为 Atlas 300V 24G。当时团队里有人质疑这卡到底行不行,有人甚至以为它跟普通显卡差不多。结果整个部署过程从驱动到 C ANN,从 ONNX 转换到推理调优,前前后后折腾了两周,踩坑踩到怀疑人生。
这里说的 Atlas,不是地图软件也不是数据库,而是昇腾(Ascend)系列里的 AI 推理加速卡。Atlas 300V 24G 这个名字里的“24G”指的是 24GB 显存版本,很多人第一次接触都会问一句“它是运算加速卡吗”——是,但它跟 GPU 的运算方式完全是两码事。这篇就把我从零开始部署 YOLO 到 Atlas 300V 24G 的完整过程写出来,包括硬件选型、软件栈安装、模型转换、推理代码、性能调优和排错速查。这套流程不只适配 YOLOv5,YOLOv8、YOLOX 等检测类模型的落地思路也完全一致。
1. Atlas 300V 24G 是什么卡,为什么部署 YOLO 选它
1.1 先回答“是运算加速卡吗”:它是专攻推理的 NPU
这个问题几乎每个刚接触昇腾的人都会问。Atlas 300V 24G 本质上是一块 PCIe 接口的 AI 加速卡,它的计算核心不是 GPU 的 CUDA Core,而是昇腾自研的达芬奇架构 AI Core。也就是说,它是一块 NPU(Neural-network Processing Unit),专门为神经网络推理设计,而不是通用并行计算卡。
这带来一个很实际的区别:拿它去跑 CUDA 程序是跑不起来的,不能用搜到的一大堆 GPU 教程直接套;但拿它去跑成熟框架导出的模型,比如 PyTorch 模型的 ONNX 导出结果,它反而有不错的能效比。24GB 这个显存容量在推理卡里算是大方了,尤其对于 YOLOv5s、YOLOv8s 这类模型,单张 640x640 输入做 FP16 推理只占几百 MB,24GB 意味着可以同时塞下多个模型副本、开大 batch,或者跑更大分辨率的输入而不用太担心显存不够。
1.2 对比 GPU 和 Jetson,为什么我最后选了它
项目选型时我们其实对比过几种方案:NVIDIA T4、Jetson Orin、还有 Atlas 300V 24G。T4 是经典的推理卡,生态成熟,资料好找,但同价位下显存通常没有 24G 这么大,而且如果项目对自主可控、国产硬件有要求,T4 这条路基本走不通;Jetson Orin 适合边缘小盒子,功耗低,但算力和多路并发能力相对有限,我们产线要接多路摄像头,它有点吃力。
Atlas 300V 24G 的优势是三块:显存大、板卡功耗相对可控、能效比在推理场景表现不错。它的短板也很明显——软件生态比 NVIDIA CUDA 生态差了不止一个量级,文档分散,版本兼容性有时候让人头大。简单说,如果你只做推理部署,且愿意为“国产化+大显存”付出一点折腾成本,它非常合适;如果你想一套代码通吃训练推理,那就别选它。
2. 部署前的软硬件准备
2.1 硬件安装与供电那些容易翻车的细节
Atlas 300V 24G 是标准 PCIe 全高全长卡,安装本身不难,但有几个细节容易翻车。首先是电源,这张卡虽然是推理卡,但满载功耗并不低,官方建议至少 300W 的电源余量,服务器电源通常没问题,千万别拿小功率台式机电源硬扛。其次,卡上有辅助供电接口,至少需要插一路,插之前看清楚接口是 PCIe 8 Pin 还是别的规格,插错直接烧卡。
另一个容易忽略的是散热风道。这张卡是被动散热居多,完全靠机箱风道带走热量,装进塔式机箱时如果旁边全是硬盘位,风道不畅就会触发高温降频。我这次用的是 4U 机架式服务器,专门给卡位配了涡轮风扇,温度控制在 70 度以内,跑长时间高负载压力测试也没有降频。
2.2 软件栈:驱动、固件、CANN 一个都不能少
硬件装好后,真正的麻烦才开始。Atlas 300V 24G 的软件栈分三块:驱动(Driver)、固件(Firmware)、CANN 工具包。CANN 是昇腾的异构计算架构,类比起来相当于 CUDA + cuDNN + TensorRT 的集合体,模型转换工具 ATC、推理运行时 ACL 都包含在 CANN 里。
安装顺序一定要对,先装驱动和固件,再装 CANN。驱动和固件通常是一个 run 包里带两个部分,或者分开两个 run 包。我这次用的环境是 Ubuntu 20.04.6,内核版本官方支持范围有限,先确认了内核版本在兼容列表里再动手,否则后面各种莫名报错会让人崩溃。
安装过程中需要 root 权限,装上之后建议建一个专门的用户,把设备文件权限处理好。命令大概是:
wget https://ascend-repo.obs.myhuaweicloud.com/.../Ascend-hdk-*.run chmod +x Ascend-hdk-*.run ./Ascend-hdk-*.run --install这里需要注意,不同型号的卡对应的驱动固件包名不一样,一定要到官网“Atlas 300V 推理卡”的页面下对应版本,而不是随便找一个大而全的“昇腾 HDK”包。下错版本的表现通常是:驱动装上了,但 n pu-smi 看不到卡,或者设备健康状态报错。
CANN 的安装更讲究版本,不能只看数字大小。CANN 分为社区版和商业版,社区版在官网能直接下,商业版需要权限申请。我们这次用的是 CANN 8.0 社区版,配套的 Python 版本建议 3.8 到 3.10,太新或者太旧都可能出现 API 对不上的问题。
安装 CANN 之前把依赖装好:
apt-get install -y python3-dev python3-pip gcc g++ make cmake ./Ascend-cann-toolkit_8.0.RC1_linux-x86_64.run --install安装完成后,环境变量要手动 source 或者写进 ~/.bashrc:
source /usr/local/Ascend/ascend-toolkit/set_env.sh这一步漏掉的话,命令行下 atc 命令会找不到,python 里 import acl 也会报模块不存在。
2.3 验证环境:npu-smi 命令怎么看
环境装完千万不要急着跑模型,先验证设备状态。npu-smi 是昇腾的设备管理工具,类似 NVIDIA 的 nvidia-smi。输入:
npu-smi info如果能看到类似下面的输出,说明驱动和固件正常:
+-------------------+-----------------+--------------------------------------------------+ | HBM-Health | OK | 100 | | Chip | 0 | 1000 MHz | | Hugepages-Total | 1 | 500 | +-------------------+-----------------+--------------------------------------------------+关键是看芯片状态是不是正常(OK),温度是不是过高,Hugepages 是否被占用。如果 npu-smi 命令不存在,说明驱动没装好或者环境变量没配上;如果能看到卡但状态是 Fault,多半是固件和驱动版本不匹配,需要重新刷固件。
3. YOLOv5 / YOLOv8 从 ONNX 到 OM 的完整流程
3.1 模型导出的关键注意点
Atlas 无法直接运行 PyTorch 的 .pt 文件,也不能直接跑 ONNX,它需要把 ONNX 模型通过 ATC 工具转换成昇腾的离线模型格式 OM。所以第一步是把训练好的 YOLOv5 或 YOLOv8 权重导出成 ONNX。
导出时第一个坑是模型版本。YOLOv5 官方仓库里 export.py 会默认导出带 NMS 检测头的 ONNX,但昇腾这边通常建议导出不带 NMS 的输出,也就是只保留模型 backbone + head 的原始输出,把 NMS 放到后处理阶段用 Python 做。这样做的好处是模型结构更纯粹,ATC 转换时算子兼容性问题会少很多,坏处是后处理代码要自己写,不过 YOLO 的 NMS 逻辑并不复杂,稍后我会给出思路。
导出命令大致是:
python export.py --weights best.pt --include onnx --opset 11这里有个非常影响后续精度的选项:opset 版本。我试过 opset 17 导出的模型在 ATC 转换时某些算子不支持,反而 opset 11 更稳妥。如果你习惯用 opset 12 以上,转换时报“Unsupport op”的概率会明显上升,所以建议固定 opset 11。
导出的 ONNX 用 netron 打开看一眼,确认输入名通常是 images,输出名通常是三个尺度的特征图(YOLOv5 一般叫 output0/YOLOv8 是 /model.24/Sigmoid_output_0 之类)。把输入输出名记下来,后面 ATC 配置要用。
3.2 AIPP 配置与 ATC 模型转换
ATC 是 Ascend Tensor Compiler 的缩写,作用类似于 TensorRT 的 model parser,把 ONNX 转成 OM 离线模型。转换之前需要准备一个关键文件:AIPP 配置。AIPP 是昇腾的图像预处理单元,可以在硬件层面完成缩放、减均值、除以标准差、色域转换等操作,省去在 Python 里做前处理的开销。
YOLOv5 部署常用的 AIPP 配置如下,保存为 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_pos_h: 0 load_start_pos_w: 0 crop_size_w: 640 crop_size_h: 640 csc_switch: true rbuv_swap_switch: false color_space: RGB }这段配置的核心作用是把输入 RGB 图像固定缩放或裁剪到 640x640,并完成色域转换。注意这里的 640 要和模型训练时的输入尺寸一致,如果你的模型是 1280x1280,那这里也要改成 1280。
ATC 转换命令示例:
atc --model=yolov5s.onnx \ --framework=5 \ --output=yolov5s_bs1 \ --input_shape="images:1,3,640,640" \ --insert_op_conf=aipp.cfg \ --soc_version=Ascend310P3 \ --output_type=FP16这里有两个容易踩的坑。第一个是 --soc_version,不同型号的卡对应不同的 SoC 版本,Atlas 300V 24G 对应的版本通常可以在 npu-smi info 里看到,或者查官方兼容列表,用错会导致转换成功但模型无法加载。第二个是 --input_shape,ONNX 导出时输入是动态的,必须在这里固定成静态 batch 和分辨率,否则生成的 OM 模型在部分场景下性能很差。
转换完成后会在当前目录生成 yolov5s_bs1.om。拿到这个文件,整个流程最难的 40% 就已经过去了。
3.3 编写推理脚本:pyACL 最简流程
OM 模型准备好之后,接下来用 Python 的 pyACL 接口来加载模型、做推理、读取结果。昇腾的 Python ACL 接口和 CUDA 的 pycuda 用起来逻辑相似,但 API 名字完全不同。
整个推理流程是:初始化 ACL -> 设置设备 -> 加载模型 -> 创建输入输出数据集 -> 前处理图像 -> 执行推理 -> 后处理 NMS。一个最简代码框架如下:
import acl import numpy as np import cv2 # 初始化 ret = acl.init() ret = acl.rt.set_device(0) context, ret = acl.rt.create_context(0) # 加载模型 model_path = "yolov5s_bs1.om" model_id, ret = acl.mdl.load_from_file(model_path) # 获取模型输入输出信息 desc = acl.mdl.create_desc() ret = acl.mdl.get_desc(desc, model_id) input_size = acl.mdl.get_num_inputs(desc) input_shape = acl.mdl.get_input_dims(desc, 0) output_size = acl.mdl.get_num_outputs(desc) output_shape = acl.mdl.get_output_dims(desc, 0) # 准备输入输出内存(这里会用到 acl.rt.malloc) ...实际代码比这复杂不少,因为要手动管理内存缓冲区、用 numpy 把数据拷进设备内存、推理完成后再拷回。我建议封装一个简单的 YOLOInference 类,把初始化、预测、释放封装起来,避免在业务代码里到处写底层的 acl API。
前处理部分,虽然 AIPP 配置已经做了缩放,但图片从 JPEG 解码到 RGB 数组这一步还是得自己来。先用 cv2 读取,再用 cv2.resize 把长边缩放到 640,短边填充到 640,填充值用 114(YOLO 训练时的默认填充值)。注意这里填充操作要放到 AIPP 的 crop 之前完成,或者说更稳妥的做法是:在 AIPP 里关闭 crop,直接把 resize + padding 后的 640x640 图像送入 AIPP,只让它做色域转换和归一化。
3.4 后处理与 NMS 的实现思路
YOLOv5 的原始输出三组特征图形状分别是 [1, 3, 80, 80, 85]、[1, 3, 40, 40, 85]、[1, 3, 20, 20, 85],其中 85 = 4 个框坐标 + 1 个目标置信度 + 80 个类别概率。后处理要做的是把这三个尺度的特征图摊平成候选框,做阈值过滤,再做 NMS。
对于 YOLOv8,输出结构有一点变化,是 [1, 84, 8400] 这样的形式,也就是把三个尺度合并成 8400 个候选框,84 = 4 + 80。处理逻辑就变成直接从 8400 个框里筛。
我在实现后处理时用了一个被验证很稳的小技巧:把输出先转成 float32 再算 sigmoid,不要直接在 float16 上做,否则少量低置信度框的精度损失会影响最终 mAP。虽然理论上 FP16 推理已经很快了,后处理转 float32 不会带来太大性能损耗,因为后处理只发生在每一帧的最终输出上,而不是神经网络计算的主循环里。
NMS 这部分直接用 numpy 写一个非极大值抑制即可,不需要引入 torch。批量处理时注意每个 batch 的框数量不同,最好先用一个 dict 按类别收集候选框,再分别做 NMS。实测下来,单张图的后处理耗时在 2 到 5 毫秒左右,对实时性影响可以接受。
4. 性能调优实录:第一版很慢,后来靠这几个参数翻倍
4.1 第一版跑出来只有不到 20 FPS,问题出在哪
我第一次把完整流程跑通时,用 640x640 输入、batch=1,测出来的推理延迟在 50 毫秒左右,也就是约 20 FPS,离 30 FPS 的目标差了一截。
当时第一个怀疑是模型优化不够,在 ATC 里没有开启混合精度和算子融合。后来查了一遍发现,真正的问题在上下文环节——日志级别默认是 INFO,昇腾的 ACL 会往 stdout 打印大量 debug 级别的加速信息,这些打印本身消耗了不小的 CPU 时间,而推理线程被 CPU 抢占,导致卡在数据拷贝和同步上。
解决办法是初始化 ACL 时把日志级别调成 ERROR:
import acl acl.init() acl.log.set_print_level(acl.ERROR) # 或者用环境变量 ASCEND_GLOBAL_LOG_LEVEL=3此外,我又用export ASCEND_SLOG_PRINT_TO_STDOUT=0把系统日志输出关掉,这一下子延迟就从 50 毫秒降到了 33 毫秒左右。
第二个问题是数据从设备到 CPU 的拷贝用同步方式做的。Python 侧从 npu 读回输出数组时,如果频繁调用 acl.rt.memcpy 并且没有合理等待,多次同步等待会白白消耗很多时间。改成用 acl.rt.memcpy_async + acl.rt.synchronize_stream 的异步方式后,又提了 5 到 8 毫秒。
4.2 静态 batch 和动态 shape 的取舍
Atlas 300V 24G 这种推理卡对静态 shape 最友好。我第一次转换模型时图省事,把输入保留成动态 shape,也就是 --input_shape="images:-1,3,-1,-1",结果推理速度和稳定性都很差。后来改成固定分辨率、固定 batch=1,模型转换时能做更多的算子融合和内存复用,性能提升非常明显。
但固定 batch=1 对多路并发是不利的。如果要用同一块卡同时处理 4 路摄像头,建议直接转一个 batch=4 的模型,推理时把 4 帧图像打包成一个输入 tensor。这样做单路延迟几乎不增加,整体吞吐量成倍提高。
一个经验:batch=4 的模型显存占用约是 batch=1 的 3 倍左右,24GB 显存跑 YOLOv8s 的 batch=4 毫无压力,还能留出不少余量跑后处理。
4.3 多路并发与流式处理方案
项目最终采用了多路视频流并发方案。我的做法是开 4 个 Python 线程,每个线程绑一个推理 context,分别把自己的输入图像搬运到设备上,然后用一个共享线程池统一执行 acl.mdl.execute(模型推理)。
这里容易忽略的是,pyACL 的 context 不能在线程间随意共享。每个线程必须创建自己的 context,保证 acl.rt.set_device 在对应线程内调用,否则会出现“ACL_ERROR_RT_CONTEXT_NULL”之类的报错。
内存方面,24GB 显存非常给力,4 路 1080p 视频流、每路做 640x640 检测,整体显存占用不到 6GB,连一半都没用到。这时候甚至可以再堆几路,或者把输入分辨率加到 1280x1280 来换取小目标检测精度的提升。
4.4 精调:AIPP 归一化是否做在硬件里
很多人会忽略 AIPP 的归一化参数。YOLOv5 训练时通常用像素值除以 255 进行归一化,也就是 [0,1] 区间。如果 AIPP 里配置了 mean 和 variance,就要把对应的 mean 设为 0、var 设为 0.00392(即 1/255)。
我一开始没有在 AIPP 里设置归一化,导致推理结果全部偏离,检测框大面积偏移或者置信度极低。后来把配置改成:
aipp_op { aipp_mode: static input_format: RGB888_U8 mean_chn_0: 0 mean_chn_1: 0 mean_chn_2: 0 var_chn_0: 0.00392157 var_chn_1: 0.00392157 var_chn_2: 0.00392157 }检测就恢复正常了。这个细节如果不在配置文件里写清楚,排查起来特别浪费时间。
5. 常见问题速查与排障思路
5.1 硬卡检测问题:装完驱动 n pu-smi 看不到卡
这种情况最常见的三个原因:驱动固件版本不匹配、卡没有正确插入 PCIe 插槽、电源供电不足。
排查步骤我总结为:
- lspci | grep -i process 看系统是否枚举到设备
- dmesg | tail -n 50 查内核日志,看有没有异常中断或供电错误
- 重新插拔卡并换一个 PCIe x16 插槽
电源供电不足时,dmesg 通常会报 voltage 或者 power 相关的错误,这时候果断换个大功率电源或者用服务器专用供电线。
5.2 ATC 转换时报算子不支持
YOLOv8 导出的 ONNX 里有时会有一些昇腾不支持的自定义算子,比如部分版本的 Focus、Silu、或者某个 PyTorch 版本导出的 Einsum。看到这类错误不用慌,先查一下算子列表。
我已经验证过,YOLOv5s 的 ONNX 在 CANN 8.0 下是可以直接转换的;YOLOv8s 的某些导出版本需要先把模型里的 SiLU 激活函数导出时转成其他等价形式,或者用更高版本的 CANN 来支持。如果你确实遇到不支持的算子,先升级 CANN 版本试试,再不行就在模型导出阶段删掉检测头只保留 backbone,把检测头的计算全部放到后处理里。
另外,ATC 转换报错信息有时候很含糊,只给一个 operator id。这时候可以用 netron 打开 ONNX,找到对应 id 附近的算子,确认是不是 Upsample、Resize 这类算子,这类算子对 opset 版本敏感。一个减少报错的经验:ONNX 导出时固定 opset=11,不仅解兼容问题,还能减少很多转换阶段的内存开销。
5.3 推理结果异常:全空检测或框严重偏移
如果你模型转换成功、推理也能跑,但检测结果完全不对,先检查 AIPP 配置。最容易出问题的三个点是:
- 像素格式:模型训练时用的是 RGB 还是 BGR,AIPP 里要对应配好,或者把 rbuv_swap_switch 设对
- 归一化参数:mean 和 var 如果不配对,输出置信度会非常低
- 输入尺寸:模型训练时是 640x640,你用 416x416 去推理,检测框必然错位
一个排查技巧是:先用一张图分别做 CPU(torch)推理和 Atlas 推理,把两者在模型输出层面的结果对比,如果差异很大,那就是前处理或 AIPP 的问题;如果输出接近但最终框不对,那就是后处理的问题。
5.4 显存或内存不足
Atlas 300V 24G 虽然显存大,但如果开太多动态 shape 模型副本,或者多路并发时每路都加载一个独立模型,同样会触发设备内存不足。我的建议是:能合并 batch 就合并,能复用模型就复用,别为每一路单独加载一个 OM。
另外,Python 侧如果频繁申请释放设备内存,会导致碎片化,时间跑久了出现“Out of Memory”但实际显存没满的情况。解决办法是启动时一次性分配好 device memory 池,或者循环复用固定的输入输出 buffer。
以下是我这次部署过程中遇到的主要问题和对应解法,整理成速查表:
| 问题现象 | 可能原因 | 解决动作 |
|---|---|---|
| n pu-smi 看不到设备 | 驱动固件版本不匹配 / 供电不足 / PCIe 松动 | 重新安装匹配版本的驱动,检查 dmesg 和 lspci,换插槽重插 |
| atc 命令找不到 | 未 source set_env.sh | source CANN 的环境变量脚本并写入 ~/.bashrc |
| ATC 转换报算子错误 | ONNX opset 过高 / 算子版本不兼容 | 固定 opset 11,尝试升级 CANN |
| 推理结果全空 | AIPP 归一化或像素格式配置错误 | 检查 mean、var、RGB/BGR 配置 |
| 推理速度只有 20 FPS | 日志级别过高 / 同步 memcpy / 动态 shape | 关闭 INFO 日志、用异步 memcpy、固定 shape |
| 运行一段时间报 OOM | 设备内存碎片化 / 动态分配过多 | 一次性分配内存池、循环复用 buffer |
| 线程间共享 context 报错 | context 不能跨线程共享 | 每个线程创建独立 context |
这些坑单看都不复杂,但串联在一起就很折磨人。尤其是 AIPP 配置这种“看着没问题、一旦配置错完全没结果”的情况,建议在实际部署前把 AIPP 相关的参数一个个验证一遍,最好能最小化测通。
最后再说一个我个人的习惯:每次拿到一块新的推理卡,我都不会直接上业务模型,而是先做一个最简的“单张图推理 + 输出固定 tensor 形状”的冒烟测试。这个测试跑通之后,再去接真实模型、真实业务逻辑。本身昇腾这套软件栈的反馈链条比较长,如果一上来就把业务耦合进来,出了问题根本不知道是模型的问题、转换的问题还是推理代码的问题。分阶段验证,看起来多花了一点时间,实际上在后面排查问题的时候至少省了一半的功夫。