最近在给公司的相册应用加一个“老照片修复”功能。起初为了省事,直接调了云端的 API,结果被用户骂惨了。不仅有隐私泄露的风险,稍微没网或者信号差一点,照片传不上去,修复功能直接变白板。痛定思痛后,我决定把 AI 模型直接塞进手机里,用端侧的 NPU 跑。但这过程,简直就是对设备极限的压榨。

模型加载的降级策略
端侧跑 AI,第一道门槛就是模型格式。云端用的 PyTorch 或者 ONNX,必须转成鸿蒙底层的 .ms 格式。为了省空间,我们还做了 FP16 甚至 INT8 的量化。

在代码里加载模型时,千万别一上来就盲目指定 NPU。有些老旧机型或者折叠屏,底层根本没有独立 NPU,强行指定会导致直接崩溃。

typescript

import mindspore from '@ohos.ai.mindSporeLite';

async function initModel(context: Context) {
  // 从 rawfile 读取模型
  const modelBuffer = await context.resourceManager.getRawFileContent('super_resolution.ms');
  
  let model = null;
  try {
    // 首选 NPU 加速
    model = await mindspore.loadModelFromBuffer(modelBuffer.buffer, {
      deviceType: mindspore.DeviceType.NPU,
      threadNum: 2
    });
  } catch (err) {
    // 警示:极其关键的降级机制,抓不到 NPU 立刻退回 CPU
    model = await mindspore.loadModelFromBuffer(modelBuffer.buffer, {
      deviceType: mindspore.DeviceType.CPU,
      threadNum: 4
    });
  }
  return model;
}

有了这段降级代码,应用在低端机和旗舰机上都能跑,不会出现一换设备就闪退的尴尬。

数据预处理的通道隔离
图片推理的输入非常严苛。你从相册拿到的 PixelMap 是 RGBA 格式,模型通常只吃 RGB,而且要求数据格式是 Float32。

如果你在 ArkTS 里写个 for 循环去遍历像素点做归一化,几千乘几千的分辨率,UI 主线程瞬间就会冻结。必须用 NAPI 把数据扔给 C++ 处理,或者利用 TaskPool 做并发。

typescript

import taskpool from '@ohos.taskpool';

@Concurrent
function preprocessImage(pixelMap: image.PixelMap): Float32Array {
  // 这里是伪代码,实际开发中建议下沉到 C++ NAPI
  const readBuffer = new ArrayBuffer(pixelMap.getPixelBytesNumber());
  pixelMap.readPixelsToBufferSync(readBuffer);
  
  const uint8Array = new Uint8Array(readBuffer);
  const floatArray = new Float32Array(1 * 3 * 224 * 224); // 假设模型输入是 1x3x224x224
  
  // 把 RGBA 的 0-255 转换为 RGB 的 0.0-1.0
  // ... 循环转换逻辑 ...
  return floatArray;
}

async function runInference(model: mindspore.Model, pixelMap: image.PixelMap) {
  // 警示:预处理必须丢给子线程,绝不能卡死 UI
  const task = new taskpool.Task(preprocessImage, pixelMap);
  const inputData = await taskpool.execute(task) as Float32Array;
  
  // 执行推理
  const outputs = await model.predict([{
    name: 'input',
    data: inputData.buffer,
    shape: [1, 3, 224, 224],
    dataType: mindspore.DataType.FLOAT32
  }]);
  
  return outputs;
}

高频推理的内存防爆
推理结束后,你拿到的是 Tensor 数组。这里有一个极其致命的警示:Tensor 对象和它的底层内存,垃圾回收器(GC)往往管不到。如果你连续处理几十张照片,底层内存直接撑爆。

每次推理完成后,只要数据已经提取出来,必须立刻调用底层的释放接口。

typescript

const outputs = await runInference(model, pixelMap);
// 取数据
const resultArray = new Float32Array(outputs[0].data);
// 核心动作:用完立刻释放,否则内存瞬间暴涨
for (let i = 0; i < outputs.length; i++) {
  outputs[i].destroy(); 
}

另外,端侧 AI 是一个绝对的重度发热大户。处理两三张图片后,手机背面就烫手了。必须在业务层做限流,或者引入防抖机制。用户如果疯狂点击“修复”按钮,千万别一次次去调推理,加个状态锁,处理完一张再处理下一张。

综合来看,端侧 AI 落地不是简单的模型移植。NPU 的容错、像素重排的并发、底层内存的暴力释放,每一个环节的疏忽都会让功能直接暴毙。只有把这些底层细节全部打磨好,用户才能享受到无网状态下、秒级响应的惊艳体验。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐