用昇腾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格式:

python 复制代码
import torch
from torchvision import models

# 加载预训练的MobileNetV3(轻量级,适合端侧)
model = models.mobilenet_v3_small(pretrained=True)
model.eval()

# 创建虚拟输入,昇腾NPU的输入格式是NCHW
dummy_input = torch.randn(1, 3, 224, 224)

# 导出ONNX
torch.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的离线模型:

bash 复制代码
# 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 -lh mobilenet_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代码加载和执行推理:

typescript 复制代码
// HarmonyInfer.ets - 鸿蒙端侧推理核心代码
import acl from '@ohos.ascendcl';

export class HarmonyInfer {
  private modelPath: string = "mobilenet_v3.om";
  private context: acl.Context | null = null;
  private model: acl.Model | null = null;

  // 初始化:加载模型
  async init(): Promise<boolean> {
    // 创建ACL Context
    this.context = acl.createContext({
      deviceId: 0,
      deviceIdType: acl.DeviceIdType.ACL_DEVICE_ID
    });

    // 加载离线模型
    this.model = acl.createModel(this.context);
    const ret = await this.model.loadFromFile(this.modelPath);
    if (!ret) {
      console.error("模型加载失败");
      return false;
    }
    console.info("模型加载成功");
    return true;
  }

  // 推理:输入图片,输出分类结果
  async infer(imageData: Uint8Array, width: number, height: number): Promise<number[]> {
    // 图片预处理:resize + normalize
    const inputTensor = this.preprocess(imageData, width, height);

    // 执行推理
    const output = await this.model.execute({
      "input": inputTensor  // 这里的"input"要和ATC转换时的input_names一致
    });

    // 后处理:softmax + argmax
    const probs = this.softmax(output["output"]);  // "output"也要和ATC的output_names一致
    const topClass = this.argmax(probs);
    return topClass;
  }

  // 预处理:缩放到224×224 + 归一化
  private preprocess(imageData: Uint8Array, w: number, h: number): acl.Tensor {
    // resize到224×224
    const resized = acl.resize(imageData, w, h, 224, 224);
    // 归一化:(pixel - mean) / std
    const mean = [0.485, 0.456, 0.406];
    const std = [0.229, 0.224, 0.225];
    const normalized = acl.normalize(resized, mean, std);
    // 转为NCHW格式的Float32 Tensor
    return acl.createTensor(normalized, {
      shape: [1, 3, 224, 224],
      dataType: acl.DataType.ACL_FLOAT16
    });
  }

  // softmax
  private softmax(logits: Float32Array): Float32Array {
    const max = Math.max(...logits);
    const exps = logits.map(x => Math.exp(x - max));
    const sum = exps.reduce((a, b) => a + b, 0);
    return new Float32Array(exps.map(x => x / sum));
  }

  // 取top-K分类
  private argmax(probs: Float32Array): number[] {
    return Array.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中调用推理

typescript 复制代码
// Index.ets - 鸿蒙App页面
@Entry
@Component
struct IndexPage {
  private infer: HarmonyInfer = new HarmonyInfer();
  private result: string = "等待推理...";

  async aboutToAppear() {
    // 页面加载时初始化模型
    await this.infer.init();
  }

  build() {
    Column() {
      Text(this.result).fontSize(24)
      Button("拍照推理")
        .onClick(async () => {
          // 调用相机拍照
          const image = await this.capturePhoto();
          // 执行推理
          const top5 = await this.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名称:

bash 复制代码
# 错误:ATC自动编号output
atc --model=model.onnx --framework=5 --output=model

# 正确:显式指定output节点名
atc --model=model.onnx --framework=5 --output=model \
    --out_nodes="output:0"  # 指定output节点

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

typescript 复制代码
// 错误
const output = result["output"];

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

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

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

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

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

bash 复制代码
# 用AMCT做量化,把FP32模型压缩为INT8
amct quantize --model=resnet50.onnx --output=resnet50_int8 --bit_width=8
atc --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版本匹配。

bash 复制代码
# 查看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-Small 5.2MB 8 120 15x
MobileNetV3-Large 12MB 15 250 17x
ResNet-18 (INT8) 6.8MB 12 180 15x
EfficientNet-B0 8.1MB 11 160 15x

结尾

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

相关推荐
nashane4 小时前
HarmonyOS 6学习:异步操作中Toast提示框消失之谜与UIContext解决方案实战
学习·华为·harmonyos
久菜盒子工作室5 小时前
华为“韬(τ)定律”
华为
松☆7 小时前
Triton推理服务接昇腾NPU,GE后端怎么搭?
华为·性能优化·numpy·信号处理·harmonyos
lqj_本人8 小时前
鸿蒙PC:从一个普通 Electron 项目到鸿蒙可运行项目:vmd-master 适配实战全记录
华为·electron·harmonyos
nashane9 小时前
HarmonyOS 6学习:听书App被“误杀”?音频焦点与AudioSession共存避坑指南
学习·音视频·harmonyos
想你依然心痛9 小时前
HarmonyOS 6 悬浮导航 + 沉浸光感:打造鸿蒙智能体驱动的沉浸式AR导航助手
ar·restful·harmonyos·智能体
lqj_本人9 小时前
鸿蒙electron框架适配PC:从桌面番茄钟到鸿蒙可用应用:Pomotroid 适配全过程复盘
华为·electron·harmonyos
dvlinker9 小时前
HarmonyOS鸿蒙PC开源QT软件移植:移植开源文本编辑器 NotePad--(Ndd)到鸿蒙 PC实践总结
harmonyos·鸿蒙pc·开源qt软件移植