快速入门

在本节中,您可以通过一个简单的图片分类应用了解使用AscendCL接口开发应用的基本过程以及开发过程中涉及的关键概念。

什么是AscendCL?

AscendCL(Ascend Computing Language)是一套用于在昇腾平台上开发深度神经网络推理应用的C语言API库,提供Device管理、Context管理、Stream管理、内存管理、模型加载与执行、算子加载与执行、媒体数据处理等API,能够实现在昇腾CANN平台上进行深度学习推理计算、图形图像预处理、单算子加速计算等能力。

了解如下基本概念

Host、Device、Context、Stream的概念如下:

了解入门应用场景

通过AscendCL提供的接口可以开发出图片分类的应用,如下:

  1. 两张需要分类的图片,需要2张原始RGB图片,且图像各参数完全匹配Resnet50输入。
  2. 一个Resnet50预训练模型文件(.om),此模型由caffe/TensorFlow等预训练模型转换而来。
  3. 最终我们将打印出推理结果中置信度前五的标签及其对应置信度,通过标签查阅其对应的类别。标签和类型的对应关系与训练模型时使用的数据集有关,需要查阅该数据集的标签及类别的对应关系。

若想直接体验该样例,请参见基于Caffe ResNet-50网络实现图片分类(同步推理)

了解开发过程

了解了入门的应用场景后,接下来可以了解下如果要开发出这个应用,主要的步骤有哪些?

  1. 准备开发环境,开发环境需要部署编译器、AscendCL组件的头文件和库文件等,便于您在完成应用的代码开发后,编译代码时使用。
  2. 开发应用,使用AscendCL提供的接口编写应用的代码逻辑。
  3. 编译代码,完成代码开发后,需要将代码编译成可执行文件,便于后续使用。
  4. 准备运行环境,运行环境上需要部署应用的可执行文件需依赖的库文件,运行应用时使用。
  5. 运行应用,最后验证应用的运行结果。

了解了这些大步骤后,下面我们再展开来说明开发应用具体涉及哪些关键功能?各功能又使用哪些AscendCL接口,这些AscendCL接口怎么串联?

当前此时您可能不理解所有细节,但这也不影响,快速入门旨在帮助您快速了解整体的代码逻辑。

开发应用时的代码逻辑

此处仅仅是关键步骤的代码示例,不可以直接拷贝编译运行,仅供参考 。

  1. include依赖的AscendCL的头文件。

    1
    #include "acl/acl.h"
    

  2. AscendCL初始化。

    使用AscendCL接口开发应用时,必须先初始化,否则可能会导致后续系统内部资源初始化出错,进而导致其它业务异常。

    aclInit接口的aclConfigPath参数是指在初始化时加载的配置文件的路径,可以通过这个配置文件来配置Dump信息用于比对精度、配置算子缓存信息老化功能用于为节约内存和平衡调用性能。当前默认是关闭Dump配置,且算子缓存信息老化配置有默认值,如果当前的默认配置已满足需求,或您不关注这些配置,可向aclInit接口中传入NULL,或者可将配置文件配置为空json串(即配置文件中只有{})。
    1
    2
    3
    4
    //此处的..表示相对路径,相对可执行文件所在的目录
    //例如,编译出来的可执行文件存放在out目录下,此处的..就表示out目录的上一级目录
    const char *aclConfigPath = "../src/acl.json";
    aclError ret = aclInit(aclConfigPath);
    

    有初始化就有去初始化,在确定完成了AscendCL的所有调用之后,或者进程退出之前,需调用接口实现AscendCL去初始化,请参见8

  3. 运行管理资源申请。

    您需要按顺序依次申请如下资源:Device、Context、Stream,确保可以使用这些资源执行运算、管理任务。

    aclrtSetDevice这个接口,调用完毕后,除了指定了计算设备之外,还会同时创建1个默认的Context;而这个默认的Context还附赠了2个Stream,1个默认Stream和1个用于执行内部同步的Stream。这也意味着如果是编写非常简单的单线程同步推理应用,在运行资源这里我们只需要调用aclrtSetDevice就够了。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    //1 指定运算的Device
    ret = aclrtSetDevice(deviceId_);
    
    //2 显式创建一个Context,用于管理Stream对象
    ret = aclrtCreateContext(&context_, deviceId_);
    
    //3 显式创建一个Stream
    //用于维护一些异步操作的执行顺序,确保按照应用程序中的代码调用顺序执行任务
    ret = aclrtCreateStream(&stream_);
    
    //4 获取当前昇腾AI软件栈的运行模式,根据不同的运行模式,后续的接口调用方式不同
    aclrtRunMode runMode;
    extern bool g_isDevice;
    ret = aclrtGetRunMode(&runMode);
    g_isDevice = (runMode == ACL_DEVICE);
    

    有运行管理资源的申请,自然也有对应的释放接口,所有数据处理都结束后,需要按顺序释放运行管理资源:Stream、Context、Device,请参见7

  4. 模型加载,并获取模型描述信息。

    如果基于网络模型开发推理应用,那就必须得先有模型,将模型加载到AscendCL中。AscendCL只认识适配昇腾AI处理器的离线模型(*.om文件),因此,模型加载前,需要将第三方网络(例如,Caffe ResNet-50网络)转换为适配昇腾AI处理器的离线模型(*.om文件),请参见ATC工具使用指南
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    //1 初始化变量
    //此处的..表示相对路径,相对可执行文件所在的目录
    //例如,编译出来的可执行文件存放在out目录下,此处的..就表示out目录的上一级目录
    const char* omModelPath = "../model/resnet50.om"
    
    //2 根据模型文件获取模型执行时所需的权值内存大小、工作内存大小。
    aclError ret = aclmdlQuerySize(omModelPath, &modelMemSize_, &modelWeightSize_);
    
    //3 根据工作内存大小,申请Device上模型执行的工作内存。
    ret = aclrtMalloc(&modelMemPtr_, modelMemSize_, ACL_MEM_MALLOC_NORMAL_ONLY);
    
    //4 根据权值内存的大小,申请Device上模型执行的权值内存。
    ret = aclrtMalloc(&modelWeightPtr_, modelWeightSize_, ACL_MEM_MALLOC_NORMAL_ONLY);
    
    //5 加载离线模型文件(适配昇腾AI处理器的离线模型),由用户自行管理模型运行的内存(包括权值内存、工作内存)。
    //“工作内存”指的是模型运行过程中临时数据所占用的内存(比如计算图,不包含权值的部分);“权值内存”,专门保存模型的权值数据。
    //模型加载成功,返回标识模型的ID。
    ret = aclmdlLoadFromFileWithMem(modelPath, &modelId_, modelMemPtr_, modelMemSize_, modelWeightPtr_, modelWeightSize_);
    
    //6 根据加载成功的模型的ID,获取该模型的描述信息,从模型的描述信息里可以获取模型的输入/输出个数等信息。
    //modelDesc_为aclmdlDesc类型。
    modelDesc_ = aclmdlCreateDesc();
    ret = aclmdlGetDesc(modelDesc_, modelId_);
    

    有加载就有卸载,模型推理结束后,需要卸载模型,请参见6

  5. 执行模型推理,并处理推理结果,打印出推理结果中置信度前五的标签及其对应置信度。

    1. 准备模型推理的输入、输出数据结构,用于描述模型的输入、输出数据。
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      //1 准备模型推理的输入数据结构
      //(1)申请输入内存
      size_t modelInputSize;
      void *modelInputBuffer = nullptr;
      //当前示例代码中的模型只有一个输入,所以index为0,如果模型有多个输入,则需要先调用aclmdlGetNumInputs接口获取模型输入的数量
      modelInputSize = aclmdlGetInputSizeByIndex(modelDesc_, 0);
      aclRet = aclrtMalloc(&modelInputBuffer, modelInputSize, ACL_MEM_MALLOC_NORMAL_ONLY);
      
      //(2)准备模型的输入数据结构
      //创建aclmdlDataset类型的数据,描述模型推理的输入,input_为aclmdlDataset类型
      input_ = aclmdlCreateDataset();
      aclDataBuffer *inputData = aclCreateDataBuffer(modelInputBuffer, modelInputSize);
      ret = aclmdlAddDatasetBuffer(input_, inputData);
      
      //2 准备模型推理的输出数据结构
      //(1)创建aclmdlDataset类型的数据,描述模型推理的输出,output_为aclmdlDataset类型
      output_ = aclmdlCreateDataset();
      
      //(2)获取模型的输出个数.
      size_t outputSize = aclmdlGetNumOutputs(modelDesc_);
      
      //(3)循环为每个输出申请内存,并将每个输出添加到aclmdlDataset类型的数据中.
      for (size_t i = 0; i < outputSize; ++i) {
          size_t buffer_size = aclmdlGetOutputSizeByIndex(modelDesc_, i);
          void *outputBuffer = nullptr;
          aclError ret = aclrtMalloc(&outputBuffer, buffer_size, ACL_MEM_MALLOC_NORMAL_ONLY);
          aclDataBuffer* outputData = aclCreateDataBuffer(outputBuffer, buffer_size);   
          ret = aclmdlAddDatasetBuffer(output_, outputData);
          }
      
    2. 准备模型推理的输入数据,进行推理,推理结束后,处理推理结果。

      向内存中读入模型推理的输入数据,再进行推理。

      推理结束后,处理推理结果,打印出推理结果中置信度前五的标签及其对应置信度。

      处理结果后,需释放模型推理的输入、输出资源,防止内存泄露。
       1
       2
       3
       4
       5
       6
       7
       8
       9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      string picturePath = "../data/dog1_1024_683.bin";
      
      //1 自定义函数ReadBinFile,调用C++标准库std::ifstream中的函数读取图片文件,输入图片文件占用的内存大小inputBuffSize以及图片文件存放在内存中的地址inputBuff
      void *inputBuff = nullptr;
      uint32_t inputBuffSize = 0;
      auto ret = Utils::ReadBinFile(picturePath, inputBuff, inputBuffSize);
          
      //2 准备模型推理的输入数据
      //在申请运行管理资源时调用aclrtGetRunMode接口获取软件栈的运行模式
      //g_isDevice参数值为true,表示软件栈运行在Device侧,无需传输图片数据或在Device内传输数据;否则,需要调用内存复制接口将数据传输到Device侧 
      if (!g_isDevice) {
          // if app is running in host, need copy data from host to device
          //modelInputBuffer、modelInputSize分别表示模型推理输入数据的内存地址、内存大小,在输入/输出数据结构准备时申请该内存
          aclError aclRet = aclrtMemcpy(modelInputBuffer, modelInputSize, inputBuff, inputBuffSize, ACL_MEMCPY_HOST_TO_DEVICE);
          (void)aclrtFreeHost(inputBuff);
      } else { // app is running in device
          aclError aclRet = aclrtMemcpy(modelInputBuffer, modelInputSize, inputBuff, inputBuffSize, ACL_MEMCPY_DEVICE_TO_DEVICE);
          (void)aclrtFree(inputBuff);
          }
      
      //3. 执行模型推理
      //modelId_表示模型ID,在模型加载成功后,会返回标识模型的ID
      //input_、output_分别表示模型推理的输入、输出数据,在准备模型推理的输入、输出数据结构时已定义
      aclError ret = aclmdlExecute(modelId_, input_, output_)
              
      //4. 处理模型推理的输出数据,输出top5置信度的类别编号 
      for (size_t i = 0; i < aclmdlGetDatasetNumBuffers(output_); ++i) {
          //获取每个输出的内存地址和内存大小
          aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(output_, i);
          void* data = aclGetDataBufferAddr(dataBuffer);
      
          size_t len = aclGetDataBufferSizeV2(dataBuffer);
      
          //将内存中的数据转换为float类型
          float *outData = NULL;
          outData = reinterpret_cast<float*>(data);
              
          //屏显每张图片的top5置信度的类别编号
          map<float, int, greater<float> > resultMap;
          for (int j = 0; j < len / sizeof(float); ++j) {
              resultMap[*outData] = j;
              outData++;
          }
          int cnt = 0;
          for (auto it = resultMap.begin(); it != resultMap.end(); ++it) {
              // print top 5
              if (++cnt > 5) {
                  break;
              }
      
              INFO_LOG("top %d: index[%d] value[%lf]", cnt, it->second, it->first);
          }
      }
      
      //5 释放模型推理的输入、输出资源
      //释放输入资源,包括数据结构和内存
      for (size_t i = 0; i < aclmdlGetDatasetNumBuffers(input_); ++i) {
              aclDataBuffer *dataBuffer = aclmdlGetDatasetBuffer(input_, i);
              (void)aclDestroyDataBuffer(dataBuffer);
      }
      (void)aclmdlDestroyDataset(input_);
      input_ = nullptr;
      aclrtFree(modelInputBuffer);
      
      //释放输出资源,包括数据结构和内存
      for (size_t i = 0; i < aclmdlGetDatasetNumBuffers(output_); ++i) {
          aclDataBuffer* dataBuffer = aclmdlGetDatasetBuffer(output_, i);
          void* data = aclGetDataBufferAddr(dataBuffer);
          (void)aclrtFree(data);
          (void)aclDestroyDataBuffer(dataBuffer);
      }
      
      (void)aclmdlDestroyDataset(output_);
      output_ = nullptr;
      

  6. 卸载模型,并释放模型描述信息。

    推理结束,需及时卸载模型,并释放相关的工作内存、权值内存。
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    //1. 卸载模型
    aclError ret = aclmdlUnload(modelId_);
    
    //2. 释放模型描述信息
    if (modelDesc_ != nullptr) {
        (void)aclmdlDestroyDesc(modelDesc_);
        modelDesc_ = nullptr;
    }
    
    //3. 释放模型运行的工作内存
    if (modelWorkPtr_ != nullptr) {
        (void)aclrtFree(modelWorkPtr_);
        modelWorkPtr_ = nullptr;
        modelWorkSize_ = 0;
    }
    
    //4. 释放模型运行的权值内存
    if (modelWeightPtr_ != nullptr) {
        (void)aclrtFree(modelWeightPtr_);
        modelWeightPtr_ = nullptr;
        modelWeightSize_ = 0;
    }
    

  7. 运行管理资源释放。

    要注意,只能销毁由aclrtCreateContext接口显式创建的Context、销毁由aclrtCreateStream接口显式创建的Stream,不能销毁默认Context、默认Stream。默认Context、默认Stream会由系统自行销毁,无需调用者关注。
    1
    2
    3
    4
    5
    6
    //1 释放Stream
    aclError ret = aclrtDestroyStream(stream_);
    //2 释放Context
    ret = aclrtDestroyContext(context_);
    //3 释放Device
    ret = aclrtResetDevice(deviceId_);
    

  8. AscendCL去初始化。

    在确定完成了AscendCL的所有调用之后,或者进程退出之前,需调用如下接口实现AscendCL去初始化。
    1
    aclError ret = aclFinalize();