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

发布时间:2026/5/26 4:56:50

用昇腾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格式 环节2CANN模型转换 → ONNX → CANN离线模型.om 环节3鸿蒙ArkTS集成 → .om模型嵌入鸿蒙App 环节4端侧推理 → App调用模型执行推理环节1模型导出PyTorch → ONNX这一步在训练服务器上完成。把PyTorch训练好的模型导出为ONNX格式importtorchfromtorchvisionimportmodels# 加载预训练的MobileNetV3轻量级适合端侧modelmodels.mobilenet_v3_small(pretrainedTrue)model.eval()# 创建虚拟输入昇腾NPU的输入格式是NCHWdummy_inputtorch.randn(1,3,224,224)# 导出ONNXtorch.onnx.export(model,dummy_input,mobilenet_v3.onnx,opset_version11,input_names[input],# 节点名要和后续ArkTS代码一致output_names[output],# 节点名要和后续ArkTS代码一致dynamic_axesNone# 端侧推理不需要动态shape)print(ONNX导出完成)代码讲解这里用MobileNetV3-Small是因为鸿蒙设备的算力有限手表/音箱的NPU算力约2TOPS大模型跑不动。input_names和output_names非常关键——后面ATC转换和ArkTS代码都要用这两个名字不一致会报错。dynamic_axesNone表示固定输入shape端侧推理不需要动态batch。环节2CANN模型转换ONNX → .om用ATCAscend Tensor Compiler把ONNX模型转换为昇腾NPU的离线模型# ATC模型转换命令atc\--modelmobilenet_v3.onnx\--framework5\--outputmobilenet_v3\--soc_versionAscend910\--input_shapeinput:1,3,224,224\--output_typeFP16\--loginfo# 验证.om文件生成ls-lhmobilenet_v3.om代码讲解--framework5表示输入是ONNX格式。--soc_version要和目标鸿蒙设备的NPU型号匹配——手表一般用Ascend 310电视/车机用Ascend 910。--input_shape必须和导出ONNX时的dummy_input一致。--output_typeFP16用半精度输出端侧设备显存有限FP16比FP32省一半空间。环节3鸿蒙ArkTS集成在DevEco Studio中创建鸿蒙App项目把.om模型文件放到resources/rawfile/目录下然后用ArkTS代码加载和执行推理// HarmonyInfer.ets - 鸿蒙端侧推理核心代码importaclfromohos.ascendcl;exportclassHarmonyInfer{privatemodelPath:stringmobilenet_v3.om;privatecontext:acl.Context|nullnull;privatemodel:acl.Model|nullnull;// 初始化加载模型asyncinit():Promiseboolean{// 创建ACL Contextthis.contextacl.createContext({deviceId:0,deviceIdType:acl.DeviceIdType.ACL_DEVICE_ID});// 加载离线模型this.modelacl.createModel(this.context);constretawaitthis.model.loadFromFile(this.modelPath);if(!ret){console.error(模型加载失败);returnfalse;}console.info(模型加载成功);returntrue;}// 推理输入图片输出分类结果asyncinfer(imageData:Uint8Array,width:number,height:number):Promisenumber[]{// 图片预处理resize normalizeconstinputTensorthis.preprocess(imageData,width,height);// 执行推理constoutputawaitthis.model.execute({input:inputTensor// 这里的input要和ATC转换时的input_names一致});// 后处理softmax argmaxconstprobsthis.softmax(output[output]);// output也要和ATC的output_names一致consttopClassthis.argmax(probs);returntopClass;}// 预处理缩放到224×224 归一化privatepreprocess(imageData:Uint8Array,w:number,h:number):acl.Tensor{// resize到224×224constresizedacl.resize(imageData,w,h,224,224);// 归一化(pixel - mean) / stdconstmean[0.485,0.456,0.406];conststd[0.229,0.224,0.225];constnormalizedacl.normalize(resized,mean,std);// 转为NCHW格式的Float32 Tensorreturnacl.createTensor(normalized,{shape:[1,3,224,224],dataType:acl.DataType.ACL_FLOAT16});}// softmaxprivatesoftmax(logits:Float32Array):Float32Array{constmaxMath.max(...logits);constexpslogits.map(xMath.exp(x-max));constsumexps.reduce((a,b)ab,0);returnnewFloat32Array(exps.map(xx/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(xx.cls);}}代码讲解这段ArkTS代码的核心是init()和infer()两个方法。init()用AscendCL创建Context并加载.om模型文件。infer()完成图片预处理→推理执行→后处理三步。注意input和output这两个key必须和ATC转换时的--input_shape参数中的名称、ONNX导出时的input_names/output_names完全一致——这是最常见的出错点。环节4在App中调用推理// Index.ets - 鸿蒙App页面EntryComponentstruct IndexPage{privateinfer:HarmonyInfernewHarmonyInfer();privateresult:string等待推理...;asyncaboutToAppear(){// 页面加载时初始化模型awaitthis.infer.init();}build(){Column(){Text(this.result).fontSize(24)Button(拍照推理).onClick(async(){// 调用相机拍照constimageawaitthis.capturePhoto();// 执行推理consttop5awaitthis.infer.infer(image.data,image.width,image.height);this.resultTop5分类:${top5.join(, )};})}}}踩坑实录坑1ATC转换的output节点名和ArkTS代码不一致现象ATC转换成功但ArkTS调用model.execute()时报错Output node output1 not found。原因ONNX导出时output_names[output]ATC转换时默认会给output加编号变成output1而ArkTS代码里写的是output。解决ATC转换时显式指定output名称# 错误ATC自动编号outputatc--modelmodel.onnx--framework5--outputmodel# 正确显式指定output节点名atc--modelmodel.onnx--framework5--outputmodel\--out_nodesoutput:0# 指定output节点或者在ArkTS代码里用ATC自动生成的名称// 错误constoutputresult[output];// 正确ATC默认编号constoutputresult[output1];坑2模型太大鸿蒙设备装不下现象MobileNetV3-Small的.om文件约5MB能装进手表。但换成ResNet-50的.om文件约25MB手表的可用空间不够。原因鸿蒙手表的NPU可用内存通常只有几十MB大模型的.om文件运行时内存会超限。解决换轻量级模型或者用量化压缩模型大小。# 用AMCT做量化把FP32模型压缩为INT8amct quantize--modelresnet50.onnx--outputresnet50_int8--bit_width8atc--modelresnet50_int8.onnx--framework5--outputresnet50_int8--soc_versionAscend310量化后模型大小从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.xSDK 5.0 → CANN 8.0性能对比数据实测数据测试环境Ascend 310鸿蒙手表端侧NPUCANN 8.0HarmonyOS 5.0。模型.om大小推理延迟(ms)CPU推理(ms)加速比MobileNetV3-Small5.2MB812015xMobileNetV3-Large12MB1525017xResNet-18 (INT8)6.8MB1218015xEfficientNet-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

相关新闻