跳到主要内容

内存二次分配管理

执行固定Shape算子示例代码

前提条件

在调用AscendCL接口执行固定Shape算子前,需提前编译算子。此处借助ATC工具编译Add算子的模型文件为例:

  1. 先构造该算子的描述文件(*.json文件,描述输入输出Tensor描述、算子属性等)。

    Add算子的描述文件示例如下:

    [
    {
    "op": "Add",
    "input_desc": [
    {
    "format": "ND",
    "shape": [8, 16],
    "type": "int32"
    },
    {
    "format": "ND",
    "shape": [8, 16],
    "type": "int32"
    }
    ],
    "output_desc": [
    {
    "format": "ND",
    "shape": [8, 16],
    "type": "int32"
    }
    ]
    }
    ]
  2. 借助ATC工具,将该算子描述文件编译成单算子模型文件(*.om文件),再分别调用AscendCL接口加载om模型文件、执行算子。

    ATC工具的命令示例如下:

    atc --singleop=$HOME/singleop/add.json --output=$HOME/singleop/out/op_model --soc_version=<soc_version>

    关键参数解释如下(详细参数说明,请参见《ATC工具使用指南》。):

    • --singleop:单算子描述文件(json格式)的路径。

    • --output:存放单算子模型文件的目录。

    • --soc_version:昇腾AI处理器的版本。

      进入“CANN软件安装目录/compiler/data/platform_config”目录,".ini"文件的文件名即为昇腾AI处理器的版本,请根据实际情况选择。

示例代码

以下是单算子加载、执行关键步骤的代码示例,不可以直接拷贝编译运行,仅供参考,调用接口后,需增加异常处理的分支,并记录报错日志、提示日志,此处不一一列举。完整代码,您可以从acl_execute_add样例中查看。

<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>27<br>28<br>29<br>30<br>31<br>32<br>33<br>34<br>35<br>36<br>37<br>38<br>39<br>40<br>41<br>42<br><br>// 1.AscendCL初始化<br>aclRet = aclInit(nullptr);<br>// 2.运行管理资源申请(使用默认Context、默认Stream,默认Stream在作为其它接口入参时,可传空指针)<br>aclRet = aclrtSetDevice(0);<br>获取软件栈的运行模式,不同运行模式影响后续的接口调用流程(例如是否进行数据传输等)<br>aclrtRunMode runMode;<br>bool g_isDevice = false;<br>aclError aclRet = aclrtGetRunMode(&runMode);<br>g_isDevice = (runMode == ACL_DEVICE);<br>// 3.加载单算子模型文件(*.om文件)<br>// 该目录相对可执行文件所在的目录,例如,编译出来的可执行文件存放在out目录下,此处就表示out/op_models目录<br>aclRet = aclopSetModelDir("op_models");<br>// 4.执行算子<br>// opType表示算子类型名称,例如Add<br>// numInputs表示算子输入个数,例如Add算子是2个输入<br>// inputDesc表示算子输入tensor描述的数组,描述每个输入的format、shape、数据类型<br>// inputs表示算子输入tensor数据<br>// numOutputs表示算子输出个数,例如Add算子是1个输出<br>// outputDesc表示算子输出tensor描述的数组,描述每个输出的format、shape、数据类型<br>// outputs表示算子输出tensor数据<br>// attr表示算子属性,如果算子没有属性,也需要调用aclopCreateAttr接口创建aclopAttr类型的数据<br>// stream用于维护一些异步操作的执行顺序<br>aclopExecuteV2(opType, numInputs, inputDesc, inputs, <br> numOutputs, outputDesc, outputs, attr, nullptr);<br>// 处理执行算子后的输出数据,例如在屏幕上显示、写入文件等,由用户根据实际情况自行实现<br>// ......<br>// 阻塞应用运行,直到指定Stream中的所有任务都完成<br>aclrtSynchronizeStream(nullptr);<br>// 5. 释放运行管理资源(默认Context、Stream无需用户释放,调用aclrtResetDevice接口后自动释放)<br>aclRet = aclrtResetDevice(0);<br>// 6.AscendCL去初始化<br>aclRet = aclFinalize();<br>// ....<br>

父主题: 单算子模型执行

执行动态Shape算子示例代码

基本原理

对于支持动态Shape的算子:

示例代码

调用接口后,需增加异常处理的分支,并记录报错日志、提示日志,此处不一一列举。以下是关键步骤的代码示例,不可以直接拷贝编译运行,仅供参考。

<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>27<br>28<br>29<br>30<br>31<br>32<br>33<br>34<br>35<br>36<br>37<br>38<br>39<br>40<br>41<br>42<br>43<br>44<br>45<br>46<br>47<br>48<br>49<br>50<br>51<br>52<br>53<br>54<br>55<br>56<br>57<br>58<br>59<br>60<br>cpp<br>// ......<br>const char *opType;<br>int numInputs;<br>aclTensorDesc *inputDesc[2];<br>aclDataBuffer *inputs[2];<br>int numOutputs;<br>aclTensorDesc *outputDesc[1];<br>aclopAttr *attr;<br>aclError ret = aclopInferShape(opType, numInputs, inputDesc, inputs,numOutputs, outputDesc, attr);<br>std::vector<std::vector<int64_t>> tensorDims; // inferShape之后的输出tensor的Shape<br>// 循环算子的每一个输出,推导或预估Shape值:<br>for (int index = 0; index < numOutputs; ++index) &#123;<br> std::vector<int64_t> dimSize; // 表示执行算子时,输出的shape<br> size_t dimNums = aclGetTensorDescNumDims(outputDesc[index]);<br> // 表示动态Shape场景下维度个数未知,该场景预留<br> if (dimNums == ACL_UNKNOWN_RANK) &#123;<br> // 由用户预估最大Shape值max shape<br> dimSize.push_back(max_shape);<br> &#125; else &#123;<br> for (size_t i = 0; i < dimNums; ++i) &#123;<br> int64_t dim;<br> ret = aclGetTensorDescDimV2(outputDesc[index], i, &dim);<br> // 表示动态Shape场景下维度值是动态的<br> if(dim == -1) &#123;<br> int64_t dimRange[2];<br> // 获取Shape范围,使用该范围中的Shape最大值来构造输出tensorDesc,作为aclopExecuteV2的输入<br> ret = aclGetTensorDescDimRange(outputDesc[index], i, 2, dimRange);<br> dim = dimRange[1];<br> &#125;<br> dimSize.push_back(dim);<br> &#125;<br> &#125;<br> tensorDims.push_back(dimSize);<br>&#125;<br>// 构造算子输入tensorDesc和输入tensor,作为aclOpExecuteV2的输入<br>aclTensorDesc *inputDescNew[2];<br>aclDataBuffer *inputsNew[2];<br>aclDataBuffer *outputsNew[1];<br>// 以上给出了执行算子时输出的shape, 根据tensorDims中的dims构造输出tensorDesc(即outputDescNew参数值), 用于调用aclopExecuteV2<br>ret = aclopExecuteV2(opType, numInputs, inputDescNew, inputsNew, numOutputs, outputDescNew, outputsNew, attr, stream);<br>// 针对上面用户预估Shape值以及使用Shape范围中的最大Shape的场景,在算子执行结束后,需增加下面的调用,获取准确的shape:<br>// for 循环每一个输出的tensorDesc<br>std::vector<std::vector<int64_t>> outTensorDims; // 准确的输出tensorShape<br>for (int index = 0; index < numOutputs; ++index) &#123;<br> std::vector<int64_t> dimSize;<br> int dimNums = aclGetTensorDescNumDims(outputDescNew[index]);<br> for (int i = 0; i < dimNums; i++)&#123;<br> int64_t dim;<br> ret = aclGetTensorDescDimV2(outputDescNew[index], i, &dim);<br> dimSize.push_back(dim);<br> &#125;<br> outTensorDims.push_back(dimSize);<br>&#125;<br>// ......<br>

父主题: 单算子模型执行

内存二次分配管理

用户内存管理有两种管理方式:

  • 独立内存管理,根据需要单独申请所需的内存,内存不做拆分或者二次分配。
  • 内存池管理内存,用户一次性申请一块较大内存,并在使用时从这块较大内存中二次分配所需内存。

在内存二次分配时,使用如下接口从内存池申请对应内存,由于接口对申请的内存地址、大小有约束,在内存池管理时,需要关注,否则容易出现内存越界。

内存管理的总体说明请参见总体说明

接口用途输入内存/输出内存
aclrtMemcpyAsync实现异步内存复制。- 调用本接口进行内存复制时,源地址和目的地址都必须64字节对齐。
aclrtMalloc在Device上申请size大小的线性内存,该接口对用户申请的size向上对齐成32字节整数倍后再多加32字节,并通过*devPtr返回已分配内存的指针。同步接口。- 若用户使用本接口申请大块内存并自行划分、管理内存时,建议使用aclrtMallocAlign32接口,该接口相比aclrtMalloc接口,只会对用户申请的size向上对齐成32字节整数倍,不会再多加32字节。 不管是aclrtMalloc接口,还是aclrtMallocAlign32接口,每段内存需同时满足以下需求:
- 内存大小向上对齐成32整数倍+32字节(m=ALIGN_UP[len,32]+32字节);
- 内存起始地址需满足64字节对齐(ALIGN_UP[m,64])。(说明:len表示某段内存的大小,ALIGN_UP[len,k]表示向上按k字节对齐:((len-1)/k+1)*k。)
acldvppMalloc该接口主要用于分配内存给Device侧媒体数据处理时使用,申请的大页内存满足数据处理的要求(例如,内存首地址128字节对齐),同步接口。 媒体数据处理各功能的详细介绍请参见媒体数据处理V1媒体数据处理的输出作为模型推理的输入时,若用户使用本接口申请大块内存并自行划分、管理内存时,每段内存需同时满足以下要求:
- 内存大小向上对齐成32整数倍+32字节(m=ALIGN_UP[len,32]+32字节);
- 内存起始地址需满足128字节对齐(ALIGN_UP[m,128])。
(说明:len表示某段内存的大小,ALIGN_UP[len,k]表示向上按k字节对齐:((len-1)/k+1)*k。)
hi_mpi_dvpp_malloc申请Device上的内存,申请的内存满足媒体数据处理的要求(例如,内存首地址128字节对齐)。 媒体数据处理各功能的详细介绍请参见媒体数据处理V2媒体数据处理的输出作为模型推理的输入时,若用户使用本接口申请大块内存并自行划分、管理时,每段内存需同时满足以下要求:
- 内存大小向上对齐成32整数倍+32字节(m=ALIGN_UP[len,32]+32字节);
- 内存起始地址需满足128字节对齐(ALIGN_UP[m,128])。
(说明:len表示某段内存的大小,ALIGN_UP[len,k]表示向上按k字节对齐:((len-1)/k+1)*k。)
aclrtMallocHost应用在Host上运行时,调用该接口申请的是Host内存,由系统保证内存首地址64字节对齐。应用在Device上运行时,调用该接口申请的是Device内存,且Device上的内存按普通页申请,如需首地址64字节对齐,需要用户自行处理对齐。同步接口。- 若用户使用本接口申请大块内存并自行划分、管理内存时,每段内存需同时满足以下需求:
- 内存大小向上对齐成32整数倍+32字节(m=ALIGN_UP[len,32]+32字节);
- 内存起始地址需满足64字节对齐(ALIGN_UP[m,64])。(说明:len表示某段内存的大小,ALIGN_UP[len,k]表示向上按k字节对齐:((len-1)/k+1)*k。)
aclrtMallocCached在Device上申请size大小的线性内存,通过*devPtr返回已分配内存的指针,该接口在任何场景下申请的内存都是支持cache缓存。同步接口。其它约束与aclrtMalloc接口相同。

在计算机视觉领域,一般涉及使用媒体数据处理功能,因此会涉及以上多种内存申请接口,内存首地址涉及64字节或128字节对齐,为方便统一管理,内存首地址对齐值建议选取较大的,比如内存首地址128字节对齐。

关于媒体数据处理时自行管理内存时的典型场景如下,媒体数据处理的功能点介绍请参见媒体数据处理V1

图1 VDEC场景

图2 JPEGD场景

父主题: 扩展更多特性

多Device切换

开发应用时,如果涉及多Device之间的任务等待,则应用程序中必须包含相关的代码逻辑,关于该场景的接口调用流程,请依次参见AscendCL接口调用流程以及本节中的说明。

图1 同步等待流程_多Device场景

父主题: 扩展更多特性

多模型串联推理

多模型推理的基本流程与单模型类似,请参见基础推理应用

多模型推理与单模型推理在AscendCL接口使用上的不同点如下:

  • 关于模型加载,如果涉及多个模型,需调用多次模型加载接口。模型加载请参见模型加载

  • 关于模型执行,如果涉及多个模型,需调用多次模型执行接口。模型执行请参见模型执行

    例如,调用aclmdlExecute接口实现同步模型推理。

父主题: 扩展更多特性

多Batch模型推理

多Batch推理的基本流程与单Batch类似,请参见基础推理应用

多Batch推理与单Batch推理的不同点在于:

  • 多Batch场景下,在构建模型时,使用ATC工具的input_shape参数需根据实际的Batch数设置batch size值,详细说明请参见《ATC工具使用指南》。

  • 在推理前,需要编写一段代码,实现逻辑为:等输入数据满足多Batch(例如:8Batch)的要求,申请Device上的内存存放多Batch的数据,作为模型推理的输入。如果最后循环遍历所有的输入数据后,仍不满足多Batch的要求,则直接将剩余数据作为模型推理的输入。

调用接口后,需增加异常处理的分支,并记录报错日志、提示日志,此处不一一列举。以下是关键步骤的代码示例,不可以直接拷贝编译运行,仅供参考,此处以8Batch为例:

<br> 1<br> 2<br> 3<br> 4<br> 5<br> 6<br> 7<br> 8<br> 9<br>10<br>11<br>12<br>13<br>14<br>15<br>16<br>17<br>18<br>19<br>20<br>21<br>22<br>23<br>24<br>25<br>26<br>27<br>28<br>29<br>30<br>31<br>32<br>33<br>34<br>35<br>36<br>37<br>38<br>39<br>40<br>41<br>42<br>43<br>44<br>45<br>46<br>47<br>48<br>49<br>50<br>51<br>52<br>53<br>54<br>55<br>56<br>57<br>58<br>59<br>cpp<br>uint32_t batchSize = 8;<br>uint32_t deviceNum = 1;<br>uint32_t deviceId = 0;<br>// 获取模型第一个输入的大小<br>uint32_t modelInputSize = aclmdlGetInputSizeByIndex(modelDesc, 0);<br>// 获取每个Batch输入数据的大小<br>uint32_t singleBuffSize = modelInputSize / batchSize;<br>// 定义该变量,用于累加batch size是否达到8Batch<br>uint32_t cnt = 0;<br>// 定义该变量,用于描述每个文件读入内存时的位置偏移<br>uint32_t pos = 0;<br>void* p_batchDst = NULL;<br>std::vector<std::string>inferFile_vec;<br>for (int i = 0; i < files.size(); ++i) <br> &#123;<br> // 每8个文件,申请一次Device上的内存,存放8Batch的输入数据 <br> if (cnt % batchSize == 0)<br> &#123;<br> pos = 0;<br> inferFile_vec.clear();<br> // 申请Device上的内存<br> ret = aclrtMalloc(&p_batchDst, modelInputSize, ACL_MEM_MALLOC_NORMAL_ONLY);<br> &#125;<br> // TODO: 从某个目录下读入文件,计算文件大小fileSize<br> <br> // 根据文件大小,申请内存,存放文件数据<br> ret = aclrtMallocHost(&p_imgBuf, fileSize);<br> // 将数据传输到Device的内存<br> ret = aclrtMemcpy((uint8_t *)p_batchDst + pos, fileSize, p_imgBuf, fileSize, ACL_MEMCPY_HOST_TO_DEVICE);<br> pos += fileSize;<br> // 及时释放不使用的内存<br> aclrtFreeHost(p_imgBuf);<br> // 将第i个文件存入vector中,同时cnt+1<br> inferFile_vec.push_back(files[i]);<br> cnt++;<br> // 每8Batch的输入数据送给模型推理进行推理<br> if (cnt % batchSize == 0)<br> &#123;<br> // TODO: 创建aclmdlDataset、aclDataBuffer类型的数据,用于描述模型的输入、输出数据<br> // TODO: 调用aclmdlExecute接口执行模型推理<br> // TODO: 推理结束后,调用aclrtFree接口释放Device上的内存<br> &#125;<br> &#125;<br>// 如果最后循环遍历所有的输入数据后,仍不满足多Batch的要求,则直接将剩余数据作为模型推理的输入。<br>if (cnt % batchSize != 0)<br> &#123;<br> // TODO: 创建aclmdlDataset、aclDataBuffer类型的数据,用于描述模型的输入、输出数据<br> // TODO: 调用aclmdlExecute接口执行模型推理<br> // TODO: 推理结束后,调用aclrtFree接口释放Device上的内存<br> &#125;<br>

父主题: 扩展更多特性

在线提单