当前位置: 首页 > 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.jsqmd.com/news/888099/

相关文章:

  • 基于U-Net与模型集成的高光谱甲烷泄漏检测系统实战解析
  • 2026年防封的营销电话系统/回拨电话系统/群呼电话系统/智能外呼电话系统榜单优选公司 - 品牌宣传支持者
  • 树莓派Pico驱动电机实战:L298N模块原理与MicroPython控制详解
  • 55项实用功能:全面解锁炉石传说自定义体验
  • 物流包装租赁共享系统的库存路径问题优化【附程序】
  • LLM API安全测试实战:从提示词注入到数据泄露的全面防御
  • Godot MCP协议:AI深度集成的游戏开发协作者
  • 21天记忆自我实验:从认知规律到高效学习系统
  • LLM API安全攻防实战:从提示词注入到自动化测试方案
  • Excel FLOOR函数原理与工程应用:向下取整≠四舍五入
  • 别再傻傻分不清了!一文搞懂USB和SCSI到底谁管谁(附BusHound实战分析)
  • 闵可夫斯基距离:统一欧氏、曼哈顿与切比雪夫的距离家族
  • Unity面部贴图工业化方案:基于Qwen-Image-Edit-F2P的UV空间对齐生成
  • 告别串口打印!用JScope的HSS模式实时图形化调试GD32F303变量(附Keil工程配置)
  • 知识图谱重构AI Agent上下文管理:从线性序列到结构化语义网络
  • PICO4 Unity打包避坑指南:SDK版本锁死与真机调试全链路解析
  • Excel单变量求解Goal Seek原理与实战指南
  • 无机布防火卷帘门价格怎么算?按尺寸定制,按需报价
  • AI邮件理解能力实测:163封真实邮件测试揭示当前技术边界与优化策略
  • 保姆级教程:用QML在QGC地面站里给姿态仪表加个航向刻度尺(附完整源码)
  • AI语音合成服务商价格暗礁图谱(含5大头部厂商阶梯价/并发限流/商用授权条款深度解析)
  • 从零到一:用PySide6和Qt Creator 4.14打造你的第一个Python GUI应用
  • R语言c()函数的底层机制与类型安全实践
  • AI Agent在智能风控中的实战:多智能体欺诈检测与预警
  • 机器学习预测核燃料热导率:从随机森林模型到UCo实验验证
  • 你的个人NAS平替方案:手把手教你用Alist搭建私有云盘聚合服务(支持WebDAV)
  • 构建去中心化GPU网络:低成本AI推理的弹性算力市场实践
  • Claude Code 2.1:仓库级认知与防错型AI编程工作流
  • ON DELETE RESTRICT:数据库参照完整性与数据丢失预防的核心实践
  • 无机布防火卷帘门报价透明,包工包料,一次说清所有费用