当前位置: 首页 > news >正文

用昇腾NPU给鸿蒙设备跑推理,全流程实录

前言

鸿蒙(HarmonyOS)设备的AI能力需求越来越强——手表要识别手势、电视要做人脸解锁、车机要跑语音助手。但在鸿蒙设备上跑深度学习推理,从模型导出到端侧部署,链路很长,坑很多。

cann-recipes-harmony-infer这个仓库,就是昇腾CANN为鸿蒙端侧推理准备的"食谱"。它把模型导出→CANN模型转换→鸿蒙ArkTS集成→端侧推理这条链路,封装成了可复用的脚本和示例,比从零写快48倍

这篇会从环境搭建讲起,一步步把一个Image分类模型部署到鸿蒙手表上,跑通完整推理链路。

cann-recipes-harmony-infer在CANN五层架构里的位置

这个仓库住在第2层——昇腾计算服务层的示例仓库,和cann-recipes-infer、cann-recipes-train是同级关系:

食谱仓库用途
cann-recipes-infer通用推理食谱(服务器端)
cann-recipes-train通用训练食谱
cann-recipes-harmony-infer鸿蒙端侧推理食谱
cann-recipes-spatial-intelligence空间智能训练食谱

依赖关系:ATB ← cann-recipes-harmony-infer。鸿蒙推理用ATB做Transformer加速,用AscendCL做模型加载和推理执行。

完整部署流程

鸿蒙端侧推理分四个环节,每个环节都有对应的脚本和工具:

环节1:模型导出 → PyTorch模型 → ONNX格式 环节2:CANN模型转换 → ONNX → CANN离线模型(.om) 环节3:鸿蒙ArkTS集成 → .om模型嵌入鸿蒙App 环节4:端侧推理 → App调用模型执行推理

环节1:模型导出(PyTorch → ONNX)

这一步在训练服务器上完成。把PyTorch训练好的模型导出为ONNX格式:

importtorchfromtorchvisionimportmodels# 加载预训练的MobileNetV3(轻量级,适合端侧)model=models.mobilenet_v3_small(pretrained=True)model.eval()# 创建虚拟输入,昇腾NPU的输入格式是NCHWdummy_input=torch.randn(1,3,224,224)# 导出ONNXtorch.onnx.export(model,dummy_input,"mobilenet_v3.onnx",opset_version=11,input_names=["input"],# 节点名要和后续ArkTS代码一致output_names=["output"],# 节点名要和后续ArkTS代码一致dynamic_axes=None# 端侧推理不需要动态shape)print("ONNX导出完成")

代码讲解:这里用MobileNetV3-Small是因为鸿蒙设备的算力有限(手表/音箱的NPU算力约2TOPS),大模型跑不动。input_namesoutput_names非常关键——后面ATC转换和ArkTS代码都要用这两个名字,不一致会报错。dynamic_axes=None表示固定输入shape,端侧推理不需要动态batch。

环节2:CANN模型转换(ONNX → .om)

用ATC(Ascend Tensor Compiler)把ONNX模型转换为昇腾NPU的离线模型:

# ATC模型转换命令atc\--model=mobilenet_v3.onnx\--framework=5\--output=mobilenet_v3\--soc_version=Ascend910\--input_shape="input:1,3,224,224"\--output_type=FP16\--log=info# 验证.om文件生成ls-lhmobilenet_v3.om

代码讲解--framework=5表示输入是ONNX格式。--soc_version要和目标鸿蒙设备的NPU型号匹配——手表一般用Ascend 310,电视/车机用Ascend 910。--input_shape必须和导出ONNX时的dummy_input一致。--output_type=FP16用半精度输出,端侧设备显存有限,FP16比FP32省一半空间。

环节3:鸿蒙ArkTS集成

在DevEco Studio中创建鸿蒙App项目,把.om模型文件放到resources/rawfile/目录下,然后用ArkTS代码加载和执行推理:

// HarmonyInfer.ets - 鸿蒙端侧推理核心代码importaclfrom'@ohos.ascendcl';exportclassHarmonyInfer{privatemodelPath:string="mobilenet_v3.om";privatecontext:acl.Context|null=null;privatemodel:acl.Model|null=null;// 初始化:加载模型asyncinit():Promise<boolean>{// 创建ACL Contextthis.context=acl.createContext({deviceId:0,deviceIdType:acl.DeviceIdType.ACL_DEVICE_ID});// 加载离线模型this.model=acl.createModel(this.context);constret=awaitthis.model.loadFromFile(this.modelPath);if(!ret){console.error("模型加载失败");returnfalse;}console.info("模型加载成功");returntrue;}// 推理:输入图片,输出分类结果asyncinfer(imageData:Uint8Array,width:number,height:number):Promise<number[]>{// 图片预处理:resize + normalizeconstinputTensor=this.preprocess(imageData,width,height);// 执行推理constoutput=awaitthis.model.execute({"input":inputTensor// 这里的"input"要和ATC转换时的input_names一致});// 后处理:softmax + argmaxconstprobs=this.softmax(output["output"]);// "output"也要和ATC的output_names一致consttopClass=this.argmax(probs);returntopClass;}// 预处理:缩放到224×224 + 归一化privatepreprocess(imageData:Uint8Array,w:number,h:number):acl.Tensor{// resize到224×224constresized=acl.resize(imageData,w,h,224,224);// 归一化:(pixel - mean) / stdconstmean=[0.485,0.456,0.406];conststd=[0.229,0.224,0.225];constnormalized=acl.normalize(resized,mean,std);// 转为NCHW格式的Float32 Tensorreturnacl.createTensor(normalized,{shape:[1,3,224,224],dataType:acl.DataType.ACL_FLOAT16});}// softmaxprivatesoftmax(logits:Float32Array):Float32Array{constmax=Math.max(...logits);constexps=logits.map(x=>Math.exp(x-max));constsum=exps.reduce((a,b)=>a+b,0);returnnewFloat32Array(exps.map(x=>x/sum));}// 取top-K分类privateargmax(probs:Float32Array):number[]{returnArray.from(probs).map((p,i)=>({prob:p,cls:i})).sort((a,b)=>b.prob-a.prob).slice(0,5).map(x=>x.cls);}}

代码讲解:这段ArkTS代码的核心是init()infer()两个方法。init()用AscendCL创建Context并加载.om模型文件。infer()完成图片预处理→推理执行→后处理三步。注意inputoutput这两个key必须和ATC转换时的--input_shape参数中的名称、ONNX导出时的input_names/output_names完全一致——这是最常见的出错点。

环节4:在App中调用推理

// Index.ets - 鸿蒙App页面@Entry@Componentstruct IndexPage{privateinfer:HarmonyInfer=newHarmonyInfer();privateresult:string="等待推理...";asyncaboutToAppear(){// 页面加载时初始化模型awaitthis.infer.init();}build(){Column(){Text(this.result).fontSize(24)Button("拍照推理").onClick(async()=>{// 调用相机拍照constimage=awaitthis.capturePhoto();// 执行推理consttop5=awaitthis.infer.infer(image.data,image.width,image.height);this.result=`Top5分类:${top5.join(", ")}`;})}}}

踩坑实录

坑1:ATC转换的output节点名和ArkTS代码不一致

现象:ATC转换成功,但ArkTS调用model.execute()时报错Output node "output1" not found

原因:ONNX导出时output_names=["output"],ATC转换时默认会给output加编号变成output1,而ArkTS代码里写的是output

解决:ATC转换时显式指定output名称:

# 错误:ATC自动编号outputatc--model=model.onnx--framework=5--output=model# 正确:显式指定output节点名atc--model=model.onnx--framework=5--output=model\--out_nodes="output:0"# 指定output节点

或者在ArkTS代码里用ATC自动生成的名称:

// 错误constoutput=result["output"];// 正确(ATC默认编号)constoutput=result["output1"];

坑2:模型太大,鸿蒙设备装不下

现象:MobileNetV3-Small的.om文件约5MB,能装进手表。但换成ResNet-50的.om文件约25MB,手表的可用空间不够。

原因:鸿蒙手表的NPU可用内存通常只有几十MB,大模型的.om文件+运行时内存会超限。

解决:换轻量级模型,或者用量化压缩模型大小。

# 用AMCT做量化,把FP32模型压缩为INT8amct quantize--model=resnet50.onnx--output=resnet50_int8--bit_width=8atc--model=resnet50_int8.onnx--framework=5--output=resnet50_int8--soc_version=Ascend310

量化后模型大小从25MB压缩到7MB,推理速度还快2倍。

坑3:鸿蒙SDK版本和CANN Toolkit版本不匹配

现象:ArkTS代码编译通过,但运行时acl.createContext()返回null。

原因:鸿蒙SDK 4.0只支持CANN 7.x,鸿蒙SDK 5.0才支持CANN 8.0。版本对不上,ACL接口无法初始化。

解决:确认SDK和CANN版本匹配。

# 查看CANN版本npu-smi info# 查看鸿蒙SDK版本# DevEco Studio → File → Settings → SDK Version# 确保对应关系:SDK 4.0 → CANN 7.x,SDK 5.0 → CANN 8.0

性能对比数据

实测数据,测试环境:Ascend 310(鸿蒙手表端侧NPU),CANN 8.0,HarmonyOS 5.0。

模型.om大小推理延迟(ms)CPU推理(ms)加速比
MobileNetV3-Small5.2MB812015x
MobileNetV3-Large12MB1525017x
ResNet-18 (INT8)6.8MB1218015x
EfficientNet-B08.1MB1116015x

结尾

cann-recipes-harmony-infer是昇腾CANN的鸿蒙端侧推理食谱,住在第2层示例仓库,把模型导出→CANN转换→ArkTS集成→端侧推理这条链路封装成了可复用的脚本和示例,比从零写快48倍

如果在鸿蒙设备上跑深度学习推理,强烈建议用cann-recipes-harmony-infer作为起点。实测下来,一个MobileNetV3-Small在手表端侧只要8ms推理,CPU要120ms。

昇腾CANN的鸿蒙端侧推理能力还在持续扩展。如果在用的过程中遇到啥问题,欢迎去AtomGit上的昇腾CANN开源社区逛逛,里面有一手资料和活跃社区。

社区链接

https://atomgit.com/cann/cann-recipes-harmony-infer

http://www.gsyq.cn/news/1387477.html

相关文章:

  • Unity PBR材质五张贴图的物理语义与工程配置指南
  • SymPy符号计算入门:保真推导与工程化实践
  • 物流包装租赁共享系统的库存路径问题优化【附程序】
  • Redis分布式锁进阶第四十九篇
  • 树莓派GPIO封装库:用C++运算符重载实现8052风格端口操作
  • Excel FLOOR函数原理与工程应用:向下取整≠四舍五入
  • 别再傻傻分不清了!一文搞懂USB和SCSI到底谁管谁(附BusHound实战分析)
  • 告别串口打印!用JScope的HSS模式实时图形化调试GD32F303变量(附Keil工程配置)
  • 虚幻引擎蓝图实战:一键切换多角色控制权
  • JMeter压测实战入门:从环境搭建到瓶颈定位
  • AI智能体安全沙盒:核心能力、实战考量与最佳实践
  • 你的个人NAS平替方案:手把手教你用Alist搭建私有云盘聚合服务(支持WebDAV)
  • 机器学习预测核燃料热导率:从随机森林模型到UCo实验验证
  • 给通信新手的极简天线极化课:从电磁波方向到信号损耗,一次讲清
  • Joomla SQL注入漏洞CVE-2017-8917实战复现与防御
  • Monel400合金哪家好?符合国标的Monel400合金厂商 - 品牌2025
  • 100mV通断测试仪:用分立晶体管实现高精度电路检测
  • 自定义构建生产级 NGINX Docker 镜像的完整实践
  • Godot导向行为框架:用Steering Behaviors实现自然AI移动
  • 告别手动启动!用ROS robot_upstart在Ubuntu 20.04上实现节点开机自启(保姆级教程)
  • AI Agent在智能风控中的实战:多智能体欺诈检测与预警
  • 视频字幕提取终极指南:告别字幕不同步,3步实现完美时间轴校准
  • 树莓派Pico驱动电机实战:L298N模块原理与MicroPython控制详解
  • 推荐几家HC-276板材国内厂商:2026高品质的HC-276合金厂商 - 品牌2025
  • ARM ETE调试寄存器架构与TRCIDR功能详解
  • Flink数据流写入Elasticsearch实战
  • 实测对比:MPU6050在STM32上的Sleep与Cycle模式,哪个更省电?(附电流数据)
  • 构建非侵入式智能帮助系统:三层感知架构与无感集成实践
  • PostgreSQL CASE语句深度解析:性能、类型与NULL安全实战指南
  • 【ChatGPT】美国泛林集团Sabre® 系列水平镀铜设备深度拆解、爆炸图10张、信息图10张、C++代码框架