系列: HarmonyOS 智能工具箱 App 全栈开发 · 第 5 篇

上一篇: AI 图像处理工具


引言

第四篇介绍了 MindSpore Lite 的基础用法。本文将深入高级推理场景:自定义模型管理、GPU/NPU 加速策略、多模型串联流水线、以及推理性能监控。这些能力让你的 App 能够灵活切换模型、充分利用硬件加速。

通过本文你将学到:

  • 管理多个模型的加载、切换和释放
  • 配置 GPU/NPU 加速推理
  • 构建多模型串联推理流水线
  • 监控推理性能指标(延迟、内存、功耗)
  • 实现模型热更新(从网络下载新模型)

环境准备

项目 版本
DevEco Studio 6.1.1 Release (6.1.1.280)
API Level API 24 (HarmonyOS 6.1.1 Release)
设备 真机(推荐麒麟芯片设备,支持 NPU 加速)

依赖配置

// oh-package.json5
{
  "dependencies": {
    "@kit.MindSporeLiteKit": "^1.0.0",
    "@kit.ImageKit": "^1.0.0",
    "@kit.CoreFileKit": "^1.0.0",
    "@kit.NetworkKit": "^1.0.0",
    "@kit.PerformanceAnalysisKit": "^1.0.0"
  }
}

模型文件准备

将预训练模型放在 resources/rawfile/ 目录下:

entry/src/main/resources/rawfile/
├── image_classifier.mindir      # 图像分类模型
├── object_detector.mindir       # 目标检测模型
├── style_transfer.mindir        # 风格迁移模型
└── labels.txt                   # 分类标签文件

模型格式:推荐 MindIR(.mindir),体积小且加载快。ONNX 模型需注意 opset 版本兼容性。


核心实现

Step 1: 创建模型管理器

目标: 统一管理多个模型的生命周期,支持按需加载和释放。

// service/ModelManager.ets
import { mindsporeLite } from '@kit.MindSporeLiteKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'ModelManager';
const DOMAIN = 0xFF00;

// 模型配置
export class ModelConfig {
  name: string = '';
  path: string = '';
  deviceType: DeviceType = DeviceType.CPU;
  numThreads: number = 4;
  inputShape: number[] = [];
  inputName: string = 'input';
  outputName: string = 'output';
}

// 推理设备类型
export enum DeviceType {
  CPU = 'cpu',
  GPU = 'gpu',
  NPU = 'npu'
}

// 推理性能指标
export class InferenceMetrics {
  modelName: string = '';
  loadTime: number = 0;
  inferenceTime: number = 0;
  preprocessTime: number = 0;
  postprocessTime: number = 0;
  totalTime: number = 0;
  memoryUsage: number = 0;
  deviceUsed: string = '';
}

// 模型实例
interface ModelInstance {
  model: mindsporeLite.Model;
  config: ModelConfig;
  loadTime: number;
  lastUsed: number;
}

export class ModelManager {
  private static models: Map<string, ModelInstance> = new Map();
  private static maxModels: number = 3;  // 最大同时加载模型数

  /**
   * 加载模型
   * @param config 模型配置
   * @returns 加载是否成功
   */
  static async loadModel(config: ModelConfig): Promise<boolean> {
    const startTime = Date.now();

    try {
      // 检查是否已加载
      if (this.models.has(config.name)) {
        hilog.info(DOMAIN, TAG, `模型 ${config.name} 已加载,跳过`);
        return true;
      }

      // 检查模型数量限制,释放最久未使用的模型
      if (this.models.size >= this.maxModels) {
        this.evictLeastUsed();
      }

      // 创建模型实例
      const model = mindsporeLite.createModel();
      await model.loadFromFile(config.path);

      // 配置推理设备
      const msConfig = mindsporeLite.createModelConfig();
      msConfig.numThreads = config.numThreads;

      switch (config.deviceType) {
        case DeviceType.GPU:
          msConfig.deviceType = mindsporeLite.DeviceType.DT_GPU;
          break;
        case DeviceType.NPU:
          msConfig.deviceType = mindsporeLite.DeviceType.DT_NPU;
          break;
        default:
          msConfig.deviceType = mindsporeLite.DeviceType.DT_CPU;
      }

      await model.build(msConfig);

      const loadTime = Date.now() - startTime;

      // 存储模型实例
      this.models.set(config.name, {
        model,
        config,
        loadTime,
        lastUsed: Date.now()
      });

      hilog.info(DOMAIN, TAG,
        `模型 ${config.name} 加载成功,设备: ${config.deviceType},耗时: ${loadTime}ms`);

      return true;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `模型 ${config.name} 加载失败: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 获取已加载的模型
   */
  static getModel(name: string): mindsporeLite.Model | null {
    const instance = this.models.get(name);
    if (instance) {
      instance.lastUsed = Date.now();
      return instance.model;
    }
    return null;
  }

  /**
   * 执行推理并收集性能指标
   */
  static async predict(
    name: string,
    inputData: Float32Array
  ): Promise<{ output: Float32Array; metrics: InferenceMetrics } | null> {
    const model = this.getModel(name);
    if (!model) {
      hilog.error(DOMAIN, TAG, `模型 ${name} 未加载`);
      return null;
    }

    const metrics = new InferenceMetrics();
    metrics.modelName = name;
    const totalStart = Date.now();

    try {
      // 推理
      const inferStart = Date.now();
      const inputs = model.createInputTensor();
      inputs[0].setData(inputData);

      const outputs = model.createOutputTensor();
      await model.predict(inputs, outputs);
      metrics.inferenceTime = Date.now() - inferStart;

      // 解析输出
      const outputData = new Float32Array(outputs[0].getData());

      metrics.totalTime = Date.now() - totalStart;
      metrics.deviceUsed = this.models.get(name)?.config.deviceType ?? 'cpu';

      return { output: outputData, metrics };
    } catch (err) {
      hilog.error(DOMAIN, TAG, `推理失败: ${JSON.stringify(err)}`);
      return null;
    }
  }

  /**
   * 释放指定模型
   */
  static releaseModel(name: string): void {
    const instance = this.models.get(name);
    if (instance) {
      instance.model.release();
      this.models.delete(name);
      hilog.info(DOMAIN, TAG, `模型 ${name} 已释放`);
    }
  }

  /**
   * 释放所有模型
   */
  static releaseAll(): void {
    this.models.forEach((instance, name) => {
      instance.model.release();
      hilog.info(DOMAIN, TAG, `释放模型: ${name}`);
    });
    this.models.clear();
  }

  /**
   * 获取已加载模型列表
   */
  static getLoadedModels(): string[] {
    return Array.from(this.models.keys());
  }

  /**
   * 释放最久未使用的模型
   */
  private static evictLeastUsed(): void {
    let oldestName = '';
    let oldestTime = Date.now();

    this.models.forEach((instance, name) => {
      if (instance.lastUsed < oldestTime) {
        oldestTime = instance.lastUsed;
        oldestName = name;
      }
    });

    if (oldestName) {
      this.releaseModel(oldestName);
      hilog.info(DOMAIN, TAG, `淘汰模型: ${oldestName}`);
    }
  }
}

Step 2: 创建多模型串联推理流水线

目标: 将多个模型串联,实现"检测→分类→风格迁移"流水线。

// service/InferencePipeline.ets
import { ModelManager, InferenceMetrics, DeviceType } from './ModelManager';
import { image } from '@kit.ImageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'InferencePipeline';
const DOMAIN = 0xFF00;

// 流水线步骤
export class PipelineStep {
  name: string = '';
  modelName: string = '';
  description: string = '';
}

// 流水线结果
export class PipelineResult {
  success: boolean = false;
  steps: StepResult[] = [];
  totalMetrics: InferenceMetrics = new InferenceMetrics();
  outputData: Float32Array = new Float32Array();
}

export class StepResult {
  stepName: string = '';
  success: boolean = false;
  output: Float32Array = new Float32Array();
  metrics: InferenceMetrics = new InferenceMetrics();
}

export class InferencePipeline {
  private steps: PipelineStep[] = [];

  /**
   * 添加流水线步骤
   */
  addStep(step: PipelineStep): void {
    this.steps.push(step);
  }

  /**
   * 执行流水线
   * @param inputImage 输入图片的 PixelMap
   * @returns 流水线结果
   */
  async execute(inputImage: image.PixelMap): Promise<PipelineResult> {
    const result = new PipelineResult();
    const totalStart = Date.now();

    hilog.info(DOMAIN, TAG, `开始执行流水线,共 ${this.steps.length}`);

    // 将图片转为 Float32Array 作为初始输入
    let currentData = await this.pixelMapToInput(inputImage);

    for (let i = 0; i < this.steps.length; i++) {
      const step = this.steps[i];
      hilog.info(DOMAIN, TAG, `执行步骤 ${i + 1}: ${step.name} (${step.modelName})`);

      const stepResult = new StepResult();
      stepResult.stepName = step.name;

      try {
        const predictResult = await ModelManager.predict(step.modelName, currentData);

        if (predictResult) {
          stepResult.output = predictResult.output;
          stepResult.metrics = predictResult.metrics;
          stepResult.success = true;

          // 当前步骤的输出作为下一步的输入
          currentData = predictResult.output;
        } else {
          stepResult.success = false;
          result.success = false;
          result.steps.push(stepResult);
          break;
        }
      } catch (err) {
        stepResult.success = false;
        result.success = false;
        hilog.error(DOMAIN, TAG, `步骤 ${step.name} 失败: ${JSON.stringify(err)}`);
        result.steps.push(stepResult);
        break;
      }

      result.steps.push(stepResult);
    }

    result.success = result.steps.every((s) => s.success);
    result.outputData = currentData;
    result.totalMetrics.totalTime = Date.now() - totalStart;

    hilog.info(DOMAIN, TAG,
      `流水线完成,总耗时: ${result.totalMetrics.totalTime}ms,成功: ${result.success}`);

    return result;
  }

  /**
   * PixelMap 转 Float32Array
   */
  private async pixelMapToInput(pixelMap: image.PixelMap): Promise<Float32Array> {
    const width = pixelMap.getWidth();
    const height = pixelMap.getHeight();
    const buffer = new ArrayBuffer(width * height * 4);
    await pixelMap.readPixelsToBuffer(buffer);

    const pixels = new Uint8Array(buffer);
    const floatData = new Float32Array(width * height * 3);

    for (let i = 0; i < width * height; i++) {
      floatData[i * 3] = pixels[i * 4] / 255.0;
      floatData[i * 3 + 1] = pixels[i * 4 + 1] / 255.0;
      floatData[i * 3 + 2] = pixels[i * 4 + 2] / 255.0;
    }

    return floatData;
  }
}

Step 3: 创建模型推理页面

目标: 实现模型管理、推理执行和性能监控的完整界面。

// features/inference/InferencePage.ets
import { ModelManager, ModelConfig, DeviceType, InferenceMetrics } from '../../service/ModelManager';
import { InferencePipeline, PipelineStep, PipelineResult } from '../../service/InferencePipeline';
import { photoAccessHelper } from '@kit.MediaLibraryKit';
import { image } from '@kit.ImageKit';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'InferencePage';

@Entry
@Component
struct InferencePage {
  @Local loadedModels: string[] = [];
  @Local selectedDevice: DeviceType = DeviceType.CPU;
  @Local isProcessing: boolean = false;
  @Local metricsHistory: InferenceMetrics[] = [];
  @Local selectedImageUri: string = '';
  @Local selectedPixelMap: image.PixelMap | undefined = undefined;
  @Local pipelineResult: string = '';

  private devices: DeviceType[] = [DeviceType.CPU, DeviceType.GPU, DeviceType.NPU];

  async aboutToAppear(): Promise<void> {
    await this.loadDefaultModels();
  }

  aboutToDisappear(): void {
    ModelManager.releaseAll();
  }

  // 加载默认模型
  async loadDefaultModels(): Promise<void> {
    const context = getContext(this);

    const configs: ModelConfig[] = [
      {
        name: 'classifier',
        path: context.resourceDir + '/rawfile/image_classifier.mindir',
        deviceType: this.selectedDevice,
        numThreads: 4,
        inputShape: [1, 3, 224, 224]
      },
      {
        name: 'detector',
        path: context.resourceDir + '/rawfile/object_detector.mindir',
        deviceType: this.selectedDevice,
        numThreads: 4,
        inputShape: [1, 3, 416, 416]
      }
    ];

    for (const config of configs) {
      await ModelManager.loadModel(config);
    }

    this.loadedModels = ModelManager.getLoadedModels();
  }

  build() {
    Column({ space: 12 }) {
      // 标题
      Row() {
        Text('端侧模型推理')
          .fontSize(24)
          .fontWeight(FontWeight.Bold)
        Blank()
      }
      .width('100%')
      .padding({ left: 16, right: 16, top: 12 })

      // 设备选择
      Column({ space: 8 }) {
        Text('推理设备').fontSize(14).fontColor('#666').width('100%')
        Row({ space: 8 }) {
          ForEach(this.devices, (device: DeviceType) => {
            Button(device.toUpperCase())
              .type(ButtonType.Normal)
              .height(32)
              .fontSize(12)
              .backgroundColor(this.selectedDevice === device ? '#007DFF' : '#F0F0F0')
              .fontColor(this.selectedDevice === device ? '#FFF' : '#333')
              .onClick(() => this.switchDevice(device))
          })
        }
      }
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(8)

      // 已加载模型列表
      Column({ space: 8 }) {
        Row() {
          Text('已加载模型').fontSize(14).fontColor('#666')
          Blank()
          Text(`${this.loadedModels.length}`).fontSize(12).fontColor('#999')
        }.width('100%')

        ForEach(this.loadedModels, (name: string) => {
          Row({ space: 8 }) {
            SymbolGlyph($r('sys.symbol.circle_fill'))
              .fontSize(8)
              .fontColor(['#4CAF50'])
            Text(name).fontSize(13)
            Blank()
            Button('释放')
              .type(ButtonType.Normal)
              .height(24)
              .fontSize(11)
              .onClick(() => {
                ModelManager.releaseModel(name);
                this.loadedModels = ModelManager.getLoadedModels();
              })
          }
          .padding(8)
          .backgroundColor('#F8F8F8')
          .borderRadius(6)
        })
      }
      .padding(12)
      .backgroundColor('#FFFFFF')
      .borderRadius(8)

      // 图片选择
      Row({ space: 8 }) {
        Button('选择图片')
          .type(ButtonType.Normal)
          .layoutWeight(1)
          .height(36)
          .onClick(() => this.pickImage())

        Button('运行推理')
          .type(ButtonType.Normal)
          .layoutWeight(1)
          .height(36)
          .enabled(!!this.selectedPixelMap && this.loadedModels.length > 0)
          .onClick(() => this.runInference())
      }
      .width('100%')

      // 推理结果
      if (this.pipelineResult) {
        Text(this.pipelineResult)
          .fontSize(13)
          .fontColor('#333')
          .padding(12)
          .backgroundColor('#F0F8FF')
          .borderRadius(8)
          .width('100%')
      }

      // 性能历史
      if (this.metricsHistory.length > 0) {
        Column({ space: 8 }) {
          Text('推理性能历史')
            .fontSize(14)
            .fontColor('#666')
            .width('100%')

          List({ space: 4 }) {
            ForEach(this.metricsHistory, (m: InferenceMetrics, index: number) => {
              ListItem() {
                Row({ space: 8 }) {
                  Text(`${index + 1}`)
                    .fontSize(12)
                    .fontColor('#999')
                    .width(20)
                  Text(m.modelName)
                    .fontSize(13)
                    .layoutWeight(1)
                  Text(`${m.inferenceTime}ms`)
                    .fontSize(13)
                    .fontColor('#007DFF')
                  Text(m.deviceUsed.toUpperCase())
                    .fontSize(11)
                    .fontColor('#999')
                    .backgroundColor('#F0F0F0')
                    .padding({ left: 6, right: 6, top: 2, bottom: 2 })
                    .borderRadius(4)
                }
                .padding(8)
                .backgroundColor('#FFFFFF')
                .borderRadius(6)
              }
            })
          }
          .height(200)
        }
        .padding(12)
        .backgroundColor('#FFFFFF')
        .borderRadius(8)
      }
    }
    .width('100%')
    .height('100%')
    .backgroundColor('#F5F5F5')
    .padding({ left: 16, right: 16 })
  }

  // 切换推理设备
  async switchDevice(device: DeviceType): Promise<void> {
    this.selectedDevice = device;
    ModelManager.releaseAll();
    await this.loadDefaultModels();
  }

  // 选择图片
  async pickImage(): Promise<void> {
    try {
      const context = getContext(this);
      const phAccessHelper = photoAccessHelper.getPhotoAccessHelper(context);
      const picker = new photoAccessHelper.PhotoViewPicker();
      const result = await picker.select({
        MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
        maxSelectNumber: 1
      });

      if (result.photoUris.length > 0) {
        this.selectedImageUri = result.photoUris[0];
        const imageSource = image.createImageSource(this.selectedImageUri);
        this.selectedPixelMap = await imageSource.createPixelMap();
      }
    } catch (err) {
      hilog.error(DOMAIN, TAG, `选图失败: ${JSON.stringify(err)}`);
    }
  }

  // 运行推理
  async runInference(): Promise<void> {
    if (!this.selectedPixelMap) return;

    this.isProcessing = true;
    this.pipelineResult = '';

    try {
      // 构建流水线
      const pipeline = new InferencePipeline();

      // 步骤 1: 分类
      pipeline.addStep({
        name: '图像分类',
        modelName: 'classifier',
        description: '对图片进行分类'
      });

      // 执行流水线
      const result = await pipeline.execute(this.selectedPixelMap);

      if (result.success) {
        // 解析分类结果
        const outputData = result.outputData;
        const maxIndex = outputData.indexOf(Math.max(...outputData));
        const confidence = outputData[maxIndex];

        const labels = ['猫', '狗', '鸟', '鱼', '马', '汽车', '飞机', '自行车', '船', '火车'];
        const className = labels[maxIndex] ?? `类别${maxIndex}`;

        this.pipelineResult = `分类结果: ${className} (${(confidence * 100).toFixed(1)}%)\n` +
          `推理耗时: ${result.totalMetrics.totalTime}ms`;

        // 记录性能
        for (const step of result.steps) {
          this.metricsHistory.unshift(step.metrics);
        }
        if (this.metricsHistory.length > 20) {
          this.metricsHistory = this.metricsHistory.slice(0, 20);
        }
      } else {
        this.pipelineResult = '推理失败,请检查模型是否加载';
      }
    } catch (err) {
      this.pipelineResult = `推理错误: ${JSON.stringify(err)}`;
    } finally {
      this.isProcessing = false;
    }
  }
}

Step 4: 实现模型热更新

目标: 支持从网络下载新模型文件并热替换。

// service/ModelUpdater.ets
import { http } from '@kit.NetworkKit';
import { fileIo as fs } from '@kit.CoreFileKit';
import { ModelManager, ModelConfig, DeviceType } from './ModelManager';
import { hilog } from '@kit.PerformanceAnalysisKit';

const TAG = 'ModelUpdater';
const DOMAIN = 0xFF00;

export class ModelUpdater {
  /**
   * 从网络下载并更新模型
   * @param modelName 模型名称
   * @param modelUrl 模型下载地址
   * @param context 应用上下文
   */
  static async downloadAndUpdate(
    modelName: string,
    modelUrl: string,
    context: Context
  ): Promise<boolean> {
    try {
      hilog.info(DOMAIN, TAG, `开始下载模型: ${modelName} from ${modelUrl}`);

      // 下载模型文件
      const httpRequest = http.createHttp();
      const response = await httpRequest.request(modelUrl, {
        method: http.RequestMethod.GET,
        expectDataType: http.HttpDataType.ARRAY_BUFFER
      });

      if (response.responseCode !== 200) {
        hilog.error(DOMAIN, TAG, `下载失败: HTTP ${response.responseCode}`);
        return false;
      }

      const modelData = response.result as ArrayBuffer;

      // 保存到应用目录
      const modelDir = context.filesDir + '/models';
      const modelPath = `${modelDir}/${modelName}.mindir`;

      // 创建目录
      try {
        fs.mkdirSync(modelDir);
      } catch (e) {
        // 目录已存在
      }

      // 写入文件
      const file = fs.openSync(modelPath, fs.OpenMode.CREATE | fs.OpenMode.WRITE_ONLY);
      fs.writeSync(file.fd, modelData);
      fs.closeSync(file);

      hilog.info(DOMAIN, TAG, `模型下载完成: ${modelPath}, 大小: ${modelData.byteLength} bytes`);

      // 释放旧模型并加载新模型
      ModelManager.releaseModel(modelName);

      const config: ModelConfig = {
        name: modelName,
        path: modelPath,
        deviceType: DeviceType.CPU,
        numThreads: 4,
        inputShape: [1, 3, 224, 224]
      };

      const success = await ModelManager.loadModel(config);
      hilog.info(DOMAIN, TAG, `模型更新${success ? '成功' : '失败'}: ${modelName}`);

      httpRequest.destroy();
      return success;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `模型更新失败: ${JSON.stringify(err)}`);
      return false;
    }
  }

  /**
   * 检查模型版本
   */
  static async checkVersion(currentVersion: string, serverUrl: string): Promise<string | null> {
    try {
      const httpRequest = http.createHttp();
      const response = await httpRequest.request(serverUrl + '/version', {
        method: http.RequestMethod.GET
      });

      if (response.responseCode === 200) {
        const data = JSON.parse(response.result as string) as Record<string, string>;
        const latestVersion = data['version'] ?? '';

        if (latestVersion !== currentVersion) {
          hilog.info(DOMAIN, TAG, `发现新版本: ${currentVersion} -> ${latestVersion}`);
          return latestVersion;
        }
      }

      httpRequest.destroy();
      return null;
    } catch (err) {
      hilog.error(DOMAIN, TAG, `版本检查失败: ${JSON.stringify(err)}`);
      return null;
    }
  }
}

Step 5: 注册路由

// pages/Index.ets 中的 navDestination 部分
.navDestination((name: string, param: Object) => {
  if (name === 'InferencePage') {
    InferencePage()
  }
})

效果展示

MobileNetV2 端侧模型推理页面

示例将 MindSpore Lite 官方 MobileNetV2 模型放入 resources/rawfile,通过 ResourceManager 读取模型字节并调用 loadModelFromBuffer(),不依赖设备上的系统绝对路径。

选择图片后,代码按模型输入形状解码为 RGBA PixelMap,转换为 RGB Float32 数据并归一化,随后执行真实 predict(),解析输出 Tensor 的 Top-5 类别编号和置信度,同时显示本次推理耗时。当前可运行代码只包含一个 CPU 模型,没有多模型 LRU、GPU/NPU 切换、性能历史和在线热更新。
在这里插入图片描述


关键代码解读

模型淘汰策略

private static evictLeastUsed(): void {
  let oldestName = '';
  let oldestTime = Date.now();

  this.models.forEach((instance, name) => {
    if (instance.lastUsed < oldestTime) {
      oldestTime = instance.lastUsed;
      oldestName = name;
    }
  });

  if (oldestName) {
    this.releaseModel(oldestName);
  }
}

采用 LRU(Least Recently Used)策略,当模型数量达到上限时自动释放最久未使用的模型,平衡内存占用和推理性能。

推理设备选择

设备 优势 适用场景
CPU 兼容性最好,所有设备支持 开发调试、小模型
GPU 并行计算强,适合图像处理 大批量图片处理
NPU 专用 AI 加速,功耗低 实时推理、移动端

注意:并非所有算子都支持 GPU/NPU,不支持的算子会自动回退到 CPU。


下篇预告

下一篇: HarmonyOS 智能工具箱(六):自然语言分析工具

将使用 Natural Language Kit 实现文本关键词提取、命名实体识别和文本分析功能。


参考资料

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐