前言
鸿蒙(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_names和output_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()完成图片预处理→推理执行→后处理三步。注意input和output这两个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开源社区逛逛,里面有一手资料和活跃社区。