aclnnBatchNormElemt
aclnnBatchNorm
接口原型
每个算子有两段接口,必须先调用“aclnnXxxGetWorkspaceSize”接口获取入参并根据计算流程计算所需workspace大小,再调用“aclnnXxx”接口执行计算。两段式接口如下:
- **第一段接口:**aclnnStatus aclnnBatchNormGetWorkspaceSize(const aclTensor *input, const aclTensor *weight, const aclTensor *bias, aclTensor *runningMean, aclTensor *runningVar, bool training, double momentum, double eps, aclTensor *output, aclTensor *saveMean, aclTensor *saveInvstd, uint64_t *workspaceSize, aclOpExecutor **executor)
- 第二段接口:aclnnStatus aclnnBatchNorm(void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, const aclrtStream stream)
功能描述
-
算子功能:对一批数据做正则化处理(Batch Normalization),处理后的数据符合标准正态分布(均值为0、标准差为1)。
-
计算公式:
其中E[x]表示均值,Var(x)表示方差,ε表示一个极小的浮点数(防止分母为0)。
aclnnBatchNormGetWorkspaceSize
-
接口定义:
aclnnStatus aclnnBatchNormGetWorkspaceSize(const aclTensor *input, const aclTensor *weight, const aclTensor *bias, aclTensor *runningMean, aclTensor *runningVar, bool training, double momentum, double eps, aclTensor *output, aclTensor *saveMean, aclTensor *saveInvstd, uint64_t *workspaceSize, aclOpExecutor **executor)
-
参数说明:
- input:Device侧的aclTensor,数据类型仅支持FLOAT、FLOAT16,支持非连续的Tensor,支持的shape和格式有:二维(对应格式为NC)、三维(对应的格式为NCL)、四维(对应的格式为NCHW)、五维(对应的格式为NCDHW)。
- weight:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- bias:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- runningMean:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- runningVar:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- training:Host侧的bool值,标记是否训练场景,True表示训练场景,False表示推理场景。
- momentum:Host侧的double值,计算滑动平均的均值。
- eps:Host侧的double值,用于防止分母为0。
- output:Device侧的aclTensor,数据类型与input一致,支持非连续的Tensor,支持的shape和格式有:二维(对应格式为NC)、三维(对应的格式为NCL)、四维(对应的格式为NCHW)、五维(对应的格式为NCDHW)。
- saveMean:Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- saveInvstd:Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- workspaceSize:返回用户需要在Device侧申请的workspace大小。
- executor:返回op执行器,包含了算子计算流程。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
:::note 说明 第一段接口完成入参校验,出现以下场景时报错:
- 返回161001(ACLNN_ERR_PARAM_NULLPTR):传入的指针类型参数是空指针。
- 返回161002(ACLNN_ERR_PARAM_INVALID):
- 参数input和output数据类型和数据格式不在支持的范围内。
- 参数input和output数据的shape不在支持的范围内。 :::
aclnnBatchNorm
-
接口定义:
aclnnStatus aclnnBatchNorm(void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, const aclrtStream stream)
-
参数说明:
- workspace:在Device侧申请的workspace内存起址。
- workspaceSize:在Device侧申请的workspace大小,由第一段接口aclnnBatchNormGetWorkspaceSize获取。
- executor:op执行器,包含了算子计算流程。
- stream:指定执行任务的AscendCL stream流。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
调用示例
<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> 61<br> 62<br> 63<br> 64<br> 65<br> 66<br> 67<br> 68<br> 69<br> 70<br> 71<br> 72<br> 73<br> 74<br> 75<br> 76<br> 77<br> 78<br> 79<br> 80<br> 81<br> 82<br> 83<br> 84<br> 85<br> 86<br> 87<br> 88<br> 89<br> 90<br> 91<br> 92<br> 93<br> 94<br> 95<br> 96<br> 97<br> 98<br> 99<br>100<br>101<br>102<br>103<br>104<br>105<br>106<br>107<br>108<br>109<br>110<br>111<br>112<br>113<br>114<br>115<br>116<br>117<br>118<br>119<br>120<br>121<br>122<br>123<br>124<br>125<br>126<br>127<br>128<br>129<br>130<br>131<br>132<br>133<br>134<br>135<br>136<br>137<br>138<br>139<br>140<br>141<br>142<br>143<br>144<br>145<br>146<br>147<br>148<br>149<br>150<br>151<br>152<br>153<br>154<br>155<br>156<br>157<br>158<br>159<br>160<br>161<br>162<br>163<br> | cpp<br>#include <iostream><br>#include <vector><br>#include "acl/acl.h"<br>#include "aclnnop/aclnn_batch_norm.h"<br>#define CHECK_RET(cond, return_expr) \<br> do { \<br> if (!(cond)) { \<br> return_expr; \<br> } \<br> } while (0)<br>#define LOG_PRINT(message, ...) \<br> do { \<br> printf(message, ##__VA_ARGS__); \<br> } while (0)<br>int64_t GetShapeSize(const std::vector<int64_t>& shape) {<br> int64_t shape_size = 1;<br> for (auto i : shape) {<br> shape_size *= i;<br> }<br> return shape_size;<br>}<br>int Init(int32_t deviceId, aclrtContext* context, aclrtStream* stream) {<br> // 固定写法,AscendCL初始化<br> auto ret = aclInit(nullptr);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetDevice(deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateContext(context, deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetCurrentContext(*context);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetCurrentContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret);<br> return 0;<br>}<br>template <typename T><br>int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr,<br> aclDataType dataType, aclTensor** tensor) {<br> auto size = GetShapeSize(shape) * sizeof(T);<br> // 调用aclrtMalloc申请device侧内存<br> auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret);<br> // 调用aclrtMemcpy将Host侧数据拷贝到device侧内存上<br> ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret);<br> // 计算连续tensor的strides<br> std::vector<int64_t> strides(shape.size(), 1);<br> for (int64_t i = shape.size() - 2; i >= 0; i--) {<br> strides[i] = shape[i + 1] * strides[i + 1];<br> }<br> // 调用aclCreateTensor接口创建aclTensor<br> *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND,<br> shape.data(), shape.size(), *deviceAddr);<br> return 0;<br>}<br>int main() {<br> // 1. (固定写法)device/context/stream初始化, 参考AscendCL对外接口列表<br> // 根据自己的实际device填写deviceId<br> int32_t deviceId = 0;<br> aclrtContext context;<br> aclrtStream stream;<br> auto ret = Init(deviceId, &context, &stream);<br> // check根据自己的需要处理<br> CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret);<br> // 2. 构造输入与输出,需要根据API的接口自定义构造<br> std::vector<int64_t> inputShape = {2, 3, 2};<br> std::vector<int64_t> meanShape = {3};<br> void* inputDeviceAddr = nullptr;<br> void* weightDeviceAddr = nullptr;<br> void* biasDeviceAddr = nullptr;<br> void* runningMeanDeviceAddr = nullptr;<br> void* runningVarDeviceAddr = nullptr;<br> void* outDeviceAddr = nullptr;<br> void* saveMeanDeviceAddr = nullptr;<br> void* saveInvstdDeviceAddr = nullptr;<br> aclTensor* input = nullptr;<br> aclTensor* weight = nullptr;<br> aclTensor* bias = nullptr;<br> aclTensor* runningMean = nullptr;<br> aclTensor* runningVar = nullptr;<br> aclTensor* out = nullptr;<br> aclTensor* saveMean = nullptr;<br> aclTensor* saveInvstd = nullptr;<br> std::vector<float> inputHostData = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};<br> std::vector<float> zeroHostData = {0, 0, 0};<br> std::vector<float> oneHostData = {1, 1, 1};<br> // 创建input aclTensor<br> ret = CreateAclTensor(inputHostData, inputShape, &inputDeviceAddr, aclDataType::ACL_FLOAT, &input);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建weight aclTensor<br> ret = CreateAclTensor(oneHostData, meanShape, &weightDeviceAddr, aclDataType::ACL_FLOAT, &weight);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建bias aclTensor<br> ret = CreateAclTensor(zeroHostData, meanShape, &biasDeviceAddr, aclDataType::ACL_FLOAT, &bias);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建runningMean aclTensor<br> ret = CreateAclTensor(zeroHostData, meanShape, &runningMeanDeviceAddr, aclDataType::ACL_FLOAT, &runningMean);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建runningVar aclTensor<br> ret = CreateAclTensor(oneHostData, meanShape, &runningVarDeviceAddr, aclDataType::ACL_FLOAT, &runningVar);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建out aclTensor<br> ret = CreateAclTensor(inputHostData, inputShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建saveMean aclTensor<br> ret = CreateAclTensor(zeroHostData, meanShape, &saveMeanDeviceAddr, aclDataType::ACL_FLOAT, &saveMean);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建saveInvstd aclTensor<br> ret = CreateAclTensor(zeroHostData, meanShape, &saveInvstdDeviceAddr, aclDataType::ACL_FLOAT, &saveInvstd);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 3.调用CANN算子库API,需要修改为具体的算子接口<br> uint64_t workspaceSize = 0;<br> aclOpExecutor* executor;<br> // 调用aclnnBatchNorm第一段接口<br> ret = aclnnBatchNormGetWorkspaceSize(input, weight, bias, runningMean, runningVar, true, 0.1, 1e-5, out, saveMean,<br> saveInvstd, &workspaceSize, &executor);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormGetWorkspaceSize failed. ERROR: %d\n", ret); return ret);<br> // 根据第一段接口计算出的workspaceSize申请device内存<br> void* workspaceAddr = nullptr;<br> if (workspaceSize > 0) {<br> ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret;);<br> }<br> // 调用aclnnBatchNorm第二段接口<br> ret = aclnnBatchNorm(workspaceAddr, workspaceSize, executor, stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNorm failed. ERROR: %d\n", ret); return ret);<br> // 4. (固定写法)同步等待任务执行结束<br> ret = aclrtSynchronizeStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret);<br> // 5. 获取输出的值,将device侧内存上的结果拷贝至Host侧,需要根据具体API的接口定义修改<br> auto size = GetShapeSize(inputShape);<br> std::vector<float> resultData(size, 0);<br> ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(float),<br> ACL_MEMCPY_DEVICE_TO_HOST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret);<br> for (int64_t i = 0; i < size; i++) {<br> LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]);<br> }<br> // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改<br> aclDestroyTensor(input);<br> aclDestroyTensor(weight);<br> aclDestroyTensor(bias);<br> aclDestroyTensor(runningMean);<br> aclDestroyTensor(runningVar);<br> aclDestroyTensor(out);<br> aclDestroyTensor(saveMean);<br> aclDestroyTensor(saveInvstd);<br> return 0;<br>}<br> |
|---|
父主题: NN类算子接口
aclnnBatchNormBackward
接口原型
每个算子有两段接口,必须先调用“aclnnXxxGetWorkspaceSize”接口获取入参并根据计算流程计算所需workspace大小,再调用“aclnnXxx”接口执行计算。两段式接口如下:
- **第一段接口:**aclnnStatus aclnnBatchNormBackwardGetWorkspaceSize(const aclTensor *gradOut, const aclTensor *input, const aclTensor *weight, const aclTensor *runningMean, const aclTensor *runningVar, const aclTensor *saveMean, const aclTensor *saveInvstd, bool training, double eps, const aclBoolArray *outputMask, aclTensor *gradInput, aclTensor *gradWeight, aclTensor *gradBias, uint64_t *workspaceSize, aclOpExecutor **executor)
- 第二段接口:aclnnStatus aclnnBatchNormBackward(void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, const aclrtStream stream)
功能描述
-
算子功能:aclnnBatchNorm的反向计算。
-
计算公式:

aclnnBatchNormBackwardGetWorkspaceSize
-
接口定义:
aclnnStatus aclnnBatchNormBackwardGetWorkspaceSize(const aclTensor *gradOut, const aclTensor *input, const aclTensor *weight, const aclTensor *runningMean, const aclTensor *runningVar, const aclTensor *saveMean, const aclTensor *saveInvstd, bool training, double eps, const aclBoolArray *outputMask, aclTensor *gradInput, aclTensor *gradWeight, aclTensor *gradBias, uint64_t *workspaceSize, aclOpExecutor **executor)
-
参数说明:
- gradOut:Device侧的aclTensor,数据类型仅支持FLOAT、FLOAT16,支持非连续的Tensor,支持的shape和格式有:二维(对应格式为NC)、三维(对应的格式为NCL)、四维(对应的格式为NCHW)、五维(对应的格式为NCDHW)。
- input:Device侧的aclTensor,数据类型仅支持FLOAT、FLOAT16,支持非连续的Tensor,支持的shape和格式有:二维(对应格式为NC)、三维(对应的格式为NCL)、四维(对应的格式为NCHW)、五维(对应的格式为NCDHW)。
- weight:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- runningMean:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- runningVar:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- saveMean:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- saveInvstd:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- training:Host侧的bool值,标记是否训练场景,True表示训练场景,False表示推理场景。
- eps:Host侧的double值,用于防止分母为0。
- outputMask:aclBoolArray类型,输出的掩码。3个bool类型的标记,分别表示是否计算gradInput、gradWeight、gradBias3个输出。
- gradInput:Device侧的aclTensor,数据类型与input一致,支持非连续的Tensor,支持的shape和格式有:二维(对应格式为NC)、三维(对应的格式为NCL)、四维(对应的格式为NCHW)、五维(对应的格式为NCDHW)。
- gradWeight:Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- gradBias:Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。shape为1维,长度与input入参中C轴的长度相等。
- workspaceSize:返回用户需要在Device侧申请的workspace大小。
- executor:返回op执行器,包含了算子计算流程。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
:::note 说明 第一段接口完成入参校验,出现以下场景时报错:
- 返回161001(ACLNN_ERR_PARAM_NULLPTR): 传入的指针类型入参是空指针。
- 返回161002(ACLNN_ERR_PARAM_INVALID):
- 参数input,gradOut,gradInput数据类型和数据格式不在支持的范围内。
- 参数input,gradOut,gradInput数据的shape不在支持的范围内。 :::
aclnnBatchNormBackward
-
接口定义:
aclnnStatus aclnnBatchNormBackward(void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, const aclrtStream stream)
-
参数说明:
- workspace:在Device侧申请的workspace内存起址。
- workspaceSize:在Device侧申请的workspace大小,由第一段接口aclnnBatchNormBackwardGetWorkspaceSize获取。
- executor:op执行器,包含了算子计算流程。
- stream:指定执行任务的AscendCL stream流。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
调用示例
<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> 61<br> 62<br> 63<br> 64<br> 65<br> 66<br> 67<br> 68<br> 69<br> 70<br> 71<br> 72<br> 73<br> 74<br> 75<br> 76<br> 77<br> 78<br> 79<br> 80<br> 81<br> 82<br> 83<br> 84<br> 85<br> 86<br> 87<br> 88<br> 89<br> 90<br> 91<br> 92<br> 93<br> 94<br> 95<br> 96<br> 97<br> 98<br> 99<br>100<br>101<br>102<br>103<br>104<br>105<br>106<br>107<br>108<br>109<br>110<br>111<br>112<br>113<br>114<br>115<br>116<br>117<br>118<br>119<br>120<br>121<br>122<br>123<br>124<br>125<br>126<br>127<br>128<br>129<br>130<br>131<br>132<br>133<br>134<br>135<br>136<br>137<br>138<br>139<br>140<br>141<br>142<br>143<br>144<br>145<br>146<br>147<br>148<br>149<br>150<br>151<br>152<br>153<br>154<br>155<br>156<br>157<br>158<br>159<br>160<br>161<br>162<br>163<br>164<br>165<br>166<br>167<br>168<br>169<br>170<br>171<br>172<br>173<br>174<br>175<br>176<br>177<br>178<br> | cpp<br>#include <iostream><br>#include <vector><br>#include "acl/acl.h"<br>#include "aclnnop/aclnn_batch_norm_backward.h"<br>#define CHECK_RET(cond, return_expr) \<br> do { \<br> if (!(cond)) { \<br> return_expr; \<br> } \<br> } while (0)<br>#define LOG_PRINT(message, ...) \<br> do { \<br> printf(message, ##__VA_ARGS__); \<br> } while (0)<br>int64_t GetShapeSize(const std::vector<int64_t>& shape) {<br> int64_t shape_size = 1;<br> for (auto i : shape) {<br> shape_size *= i;<br> }<br> return shape_size;<br>}<br>int Init(int32_t deviceId, aclrtContext* context, aclrtStream* stream) {<br> // 固定写法,AscendCL初始化<br> auto ret = aclInit(nullptr);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetDevice(deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateContext(context, deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetCurrentContext(*context);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetCurrentContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret);<br> return 0;<br>}<br>template <typename T><br>int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr,<br> aclDataType dataType, aclTensor** tensor) {<br> auto size = GetShapeSize(shape) * sizeof(T);<br> // 调用aclrtMalloc申请device侧内存<br> auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret);<br> // 调用aclrtMemcpy将Host侧数据拷贝到device侧内存上<br> ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret);<br> // 计算连续tensor的strides<br> std::vector<int64_t> strides(shape.size(), 1);<br> for (int64_t i = shape.size() - 2; i >= 0; i--) {<br> strides[i] = shape[i + 1] * strides[i + 1];<br> }<br> // 调用aclCreateTensor接口创建aclTensor<br> *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND,<br> shape.data(), shape.size(), *deviceAddr);<br> return 0;<br>}<br>int main() {<br> // 1. (固定写法)device/context/stream初始化, 参考AscendCL对外接口列表<br> // 根据自己的实际device填写deviceId<br> int32_t deviceId = 0;<br> aclrtContext context;<br> aclrtStream stream;<br> auto ret = Init(deviceId, &context, &stream);<br> // check根据自己的需要处理<br> CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret);<br> // 2. 构造输入与输出,需要根据API的接口自定义构造<br> std::vector<int64_t> inputShape = {2, 3, 2};<br> std::vector<int64_t> meanShape = {3};<br> void* gradOutDeviceAddr = nullptr;<br> void* inputDeviceAddr = nullptr;<br> void* weightDeviceAddr = nullptr;<br> void* runningMeanDeviceAddr = nullptr;<br> void* runningVarDeviceAddr = nullptr;<br> void* saveMeanDeviceAddr = nullptr;<br> void* saveInvstdDeviceAddr = nullptr;<br> void* gradInputDeviceAddr = nullptr;<br> void* gradWeightDeviceAddr = nullptr;<br> void* gradBiasDeviceAddr = nullptr;<br> aclTensor* gradOut = nullptr;<br> aclTensor* input = nullptr;<br> aclTensor* weight = nullptr;<br> aclTensor* runningMean = nullptr;<br> aclTensor* runningVar = nullptr;<br> aclTensor* saveMean = nullptr;<br> aclTensor* saveInvstd = nullptr;<br> aclTensor* gradInput = nullptr;<br> aclTensor* gradWeight = nullptr;<br> aclTensor* gradBias = nullptr;<br> std::vector<float> inputHostData = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};<br> std::vector<float> zeroHostData = {0, 0, 0};<br> std::vector<float> oneHostData = {1, 1, 1};<br> // 创建gradOut aclTensor<br> ret = CreateAclTensor(inputHostData, inputShape, &gradOutDeviceAddr, aclDataType::ACL_FLOAT, &gradOut);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建input aclTensor<br> ret = CreateAclTensor(inputHostData, inputShape, &inputDeviceAddr, aclDataType::ACL_FLOAT, &input);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建weight aclTensor<br> ret = CreateAclTensor(oneHostData, meanShape, &weightDeviceAddr, aclDataType::ACL_FLOAT, &weight);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建runningMean aclTensor<br> ret = CreateAclTensor(zeroHostData, meanShape, &runningMeanDeviceAddr, aclDataType::ACL_FLOAT, &runningMean);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建runningVar aclTensor<br> ret = CreateAclTensor(oneHostData, meanShape, &runningVarDeviceAddr, aclDataType::ACL_FLOAT, &runningVar);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建saveMean aclTensor<br> ret = CreateAclTensor(zeroHostData, meanShape, &saveMeanDeviceAddr, aclDataType::ACL_FLOAT, &saveMean);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建saveInvstd aclTensor<br> ret = CreateAclTensor(zeroHostData, meanShape, &saveInvstdDeviceAddr, aclDataType::ACL_FLOAT, &saveInvstd);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建gradInput aclTensor<br> ret = CreateAclTensor(inputHostData, inputShape, &gradInputDeviceAddr, aclDataType::ACL_FLOAT, &gradInput);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建gradWeight aclTensor<br> ret = CreateAclTensor(zeroHostData, meanShape, &gradWeightDeviceAddr, aclDataType::ACL_FLOAT, &gradWeight);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建gradBias aclTensor<br> ret = CreateAclTensor(zeroHostData, meanShape, &gradBiasDeviceAddr, aclDataType::ACL_FLOAT, &gradBias);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> std::array<bool, 3> value = {true, true, true};<br> auto outputMask = aclCreateBoolArray(value.data(), value.size());<br> // 3.调用CANN算子库API,需要修改为具体的算子接口<br> uint64_t workspaceSize = 0;<br> aclOpExecutor* executor;<br> // 调用aclnnBatchNormBackward第一段接口<br> ret = aclnnBatchNormBackwardGetWorkspaceSize(gradOut, input, weight, runningMean, runningVar, saveMean, saveInvstd,<br> true, 1e-5, outputMask, gradInput, gradWeight, gradBias,<br> &workspaceSize, &executor);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormBackwardGetWorkspaceSize failed. ERROR: %d\n", ret); return ret);<br> // 根据第一段接口计算出的workspaceSize申请device内存<br> void* workspaceAddr = nullptr;<br> if (workspaceSize > 0) {<br> ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret;);<br> }<br> // 调用aclnnBatchNormBackward第二段接口<br> ret = aclnnBatchNormBackward(workspaceAddr, workspaceSize, executor, stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormBackward failed. ERROR: %d\n", ret); return ret);<br> // 4. (固定写法)同步等待任务执行结束<br> ret = aclrtSynchronizeStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret);<br> // 5. 获取输出的值,将device侧内存上的结果拷贝至Host侧,需要根据具体API的接口定义修改<br> auto size = GetShapeSize(inputShape);<br> std::vector<float> resultData(size, 0);<br> ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), gradInput, size * sizeof(float),<br> ACL_MEMCPY_DEVICE_TO_HOST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret);<br> for (int64_t i = 0; i < size; i++) {<br> LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]);<br> }<br> // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改<br> aclDestroyTensor(gradOut);<br> aclDestroyTensor(input);<br> aclDestroyTensor(weight);<br> aclDestroyTensor(runningMean);<br> aclDestroyTensor(runningVar);<br> aclDestroyTensor(saveMean);<br> aclDestroyTensor(saveInvstd);<br> aclDestroyTensor(gradInput);<br> aclDestroyTensor(gradWeight);<br> aclDestroyTensor(gradBias);<br> return 0;<br>}<br> |
|---|
父主题: NN类算子接口
aclnnBatchNormElemt
接口原型
每个算子有两段接口,必须先调用“aclnnXxxGetWorkspaceSize”接口获取入参并根据计算流程计算所需workspace大小,再调用“aclnnXxx”接口执行计算。两段式接口如下:
- 第一段接口:aclnnStatus aclnnBatchNormElemtGetWorkspaceSize(const aclTensor* input, const aclTensor* weight, const aclTensor* bias, aclTensor* mean, aclTensor* invstd, float eps, aclTensor* output, uint64_t* workspaceSize, aclOpExecutor** executor)
- 第二段接口:aclnnStatus aclnnBatchNormElemt(void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)
功能描述
-
算子功能:根据全局的均值和标准差倒数对输入数据做BN,走推理模式。多卡BatchNorm场景下实现SyncBatchNorm,需通过aclnnBatchNormStats、aclnnBatchNormGatherStatsWithCounts和aclnnBatchNormElemt算子组合实现。
:::note 说明 BatchNorm的性能和BatchSize相关,BatchSize越大,BatchNorm的统计量也会越准。对于目标检测类似的任务,占用显存较高,一张显卡往往只能使用较少的图片(比如2张)来训练,这就导致BN的表现变差。为解决该问题,需要实现SyncBatchNorm,即所有卡共享同一个BN,得到全局的统计量。 :::
-
计算公式:
其中标准差与方差的关系如下:
aclnnBatchNormElemtGetWorkspaceSize
-
接口定义:
aclnnStatus aclnnBatchNormElemtGetWorkspaceSize(const aclTensor* input, const aclTensor* weight, const aclTensor* bias, aclTensor* mean, aclTensor* invstd, float eps, aclTensor* output, uint64_t* workspaceSize, aclOpExecutor** executor)
-
参数说明:
- self:Device侧的aclTensor,数据类型支持FLOAT16、FLOAT,支持非连续Tensor,数据格式五维及以下支持NCDHW、NCHW、NCL、NC,六维到八维支持ND。
- weight:可选参数,BN权重,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。Shape为一维,长度和入参self中C轴的长度相同。
- bias:可选参数,BN偏置,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。Shape为一维,长度和入参self中C轴的长度相同。
- mean:输入数据均值,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。Shape为一维,长度和入参self中C轴的长度相同。
- invstd:输入数据标准差的倒数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。Shape为一维,长度和入参self中C轴的长度相同。invstd大于0时,和Cuda行为保持一致,其余情况不做保证。
- eps:用于防止产生除0的偏移,Host侧的float值,默认值为1e-5。
- output:Device侧的aclTensor,数据类型与input一致,支持非连续Tensor,数据格式与输入一致。
- workspaceSize:返回用户需要在Device侧申请的workspace大小。
- executor:返回op执行器,包含了算子计算流程。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
:::note 说明 第一段接口完成入参校验,出现以下场景时报错:
- 返回161001(ACLNN_ERR_PARAM_NULLPTR):传入的指针类型入参是空指针。
- 返回161002(ACLNN_ERR_PARAM_INVALID):
- input、output数据类型和数据格式不在支持的范围内。
- input、output数据的shape不在支持的范围内。
- mean、invstd、weight、bias维度不为一维,并且shape不等于入参C轴的长度。 :::
aclnnBatchNormElemt
-
接口定义:
aclnnStatus aclnnBatchNormElemt(void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)
-
参数说明:
- workspace:在Device侧申请的workspace内存起址。
- workspaceSize:在Device侧申请的workspace大小,由第一段接口aclnnBatchNormElemtGetWorkspaceSize获取。
- executor:op执行器,包含了算子计算流程。
- stream:指定执行任务的AscendCL stream流。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
调用示例
<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> 61<br> 62<br> 63<br> 64<br> 65<br> 66<br> 67<br> 68<br> 69<br> 70<br> 71<br> 72<br> 73<br> 74<br> 75<br> 76<br> 77<br> 78<br> 79<br> 80<br> 81<br> 82<br> 83<br> 84<br> 85<br> 86<br> 87<br> 88<br> 89<br> 90<br> 91<br> 92<br> 93<br> 94<br> 95<br> 96<br> 97<br> 98<br> 99<br>100<br>101<br>102<br>103<br>104<br>105<br>106<br>107<br>108<br>109<br>110<br>111<br>112<br>113<br>114<br>115<br>116<br>117<br>118<br>119<br>120<br>121<br>122<br>123<br>124<br>125<br>126<br>127<br>128<br>129<br>130<br>131<br>132<br>133<br>134<br>135<br>136<br>137<br>138<br>139<br>140<br>141<br>142<br> | cpp<br>#include <iostream><br>#include <vector><br>#include "acl/acl.h"<br>#include "aclnnop/aclnn_batch_norm_elemt.h"<br>#define CHECK_RET(cond, return_expr) \<br> do { \<br> if (!(cond)) { \<br> return_expr; \<br> } \<br> } while (0)<br>#define LOG_PRINT(message, ...) \<br> do { \<br> printf(message, ##__VA_ARGS__); \<br> } while (0)<br>int64_t GetShapeSize(const std::vector<int64_t>& shape) {<br> int64_t shape_size = 1;<br> for (auto i : shape) {<br> shape_size *= i;<br> }<br> return shape_size;<br>}<br>int Init(int32_t deviceId, aclrtContext* context, aclrtStream* stream) {<br> // 固定写法,AscendCL初始化<br> auto ret = aclInit(nullptr);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetDevice(deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateContext(context, deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetCurrentContext(*context);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetCurrentContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret);<br> return 0;<br>}<br>template <typename T><br>int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr,<br> aclDataType dataType, aclTensor** tensor) {<br> auto size = GetShapeSize(shape) * sizeof(T);<br> // 调用aclrtMalloc申请device侧内存<br> auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret);<br> // 调用aclrtMemcpy将Host侧数据拷贝到device侧内存上<br> ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret);<br> // 计算连续tensor的strides<br> std::vector<int64_t> strides(shape.size(), 1);<br> for (int64_t i = shape.size() - 2; i >= 0; i--) {<br> strides[i] = shape[i + 1] * strides[i + 1];<br> }<br> // 调用aclCreateTensor接口创建aclTensor<br> *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND,<br> shape.data(), shape.size(), *deviceAddr);<br> return 0;<br>}<br>int main() {<br> // 1. (固定写法)device/context/stream初始化, 参考AscendCL对外接口列表<br> // 根据自己的实际device填写deviceId<br> int32_t deviceId = 0;<br> aclrtContext context;<br> aclrtStream stream;<br> auto ret = Init(deviceId, &context, &stream);<br> // check根据自己的需要处理<br> CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret);<br> // 2. 构造输入与输出,需要根据API的接口自定义构造<br> std::vector<int64_t> inputShape = {2, 4, 2};<br> std::vector<int64_t> meanShape = {4};<br> std::vector<int64_t> invstdShape = {4};<br> std::vector<int64_t> outShape = {2, 4, 2};<br> double eps = 1e-2;<br> void* inputDeviceAddr = nullptr;<br> void* meanDeviceAddr = nullptr;<br> void* invstdDeviceAddr = nullptr;<br> void* outDeviceAddr = nullptr;<br> aclTensor* input = nullptr;<br> aclTensor* mean = nullptr;<br> aclTensor* invstd = nullptr;<br> aclTensor* out = nullptr;<br> std::vector<float> inputHostData = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};<br> std::vector<float> meanHostData = {1, 2, 3, 4};<br> std::vector<float> invstdHostData = {5, 6, 7, 8};<br> std::vector<float> outHostData = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}; <br> // 创建input aclTensor<br> ret = CreateAclTensor(inputHostData, inputShape, &inputDeviceAddr, aclDataType::ACL_FLOAT, &input);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建mean aclTensor<br> ret = CreateAclTensor(meanHostData, meanShape, &meanDeviceAddr, aclDataType::ACL_FLOAT, &mean);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建invstd aclTensor<br> ret = CreateAclTensor(invstdHostData, invstdShape, &invstdDeviceAddr, aclDataType::ACL_FLOAT, &invstd);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建out aclTensor<br> ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 3.调用CANN算子库API,需要修改为具体的算子接口<br> uint64_t workspaceSize = 0;<br> aclOpExecutor* executor;<br> // 调用aclnnBatchNormElemt第一段接口<br> ret = aclnnBatchNormElemtGetWorkspaceSize(input, nullptr, nullptr, mean, invstd, eps, out, &workspaceSize, &executor);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormElemtGetWorkspaceSize failed. ERROR: %d\n", ret); return ret);<br> // 根据第一段接口计算出的workspaceSize申请device内存<br> void* workspaceAddr = nullptr;<br> if (workspaceSize > 0) {<br> ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret;);<br> }<br> // 调用aclnnBatchNormElemt第二段接口<br> ret = aclnnBatchNormElemt(workspaceAddr, workspaceSize, executor, stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormElemt failed. ERROR: %d\n", ret); return ret);<br> // 4. (固定写法)同步等待任务执行结束<br> ret = aclrtSynchronizeStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret);<br> // 5. 获取输出的值,将device侧内存上的结果拷贝至Host侧,需要根据具体API的接口定义修改<br> auto size = GetShapeSize(outShape);<br> std::vector<float> resultData(size, 0);<br> ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(float),<br> ACL_MEMCPY_DEVICE_TO_HOST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret);<br> for (int64_t i = 0; i < size; i++) {<br> LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]);<br> }<br> // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改<br> aclDestroyTensor(input);<br> aclDestroyTensor(mean);<br> aclDestroyTensor(invstd);<br> aclDestroyTensor(out);<br> return 0;<br>}<br> |
|---|
父主题: NN类算子接口
aclnnBatchNormElemtBackward
接口原型
每个算子有两段接口,必须先调用“aclnnXxxGetWorkspaceSize”接口获取入参并根据计算流程计算所需workspace大小,再调用“aclnnXxx”接口执行计算。两段式接口如下:
- **第一段接口:**aclnnStatus aclnnBatchNormElemtBackwardGetWorkspaceSize(const aclTensor* gradOut, const aclTensor* input, const aclTensor* mean, const aclTensor* invstd, const aclTensor* weight, const aclTensor* meanDy, const aclTensor* meanDyXmu, aclTensor* gradInput,uint64_t* workspaceSize, aclOpExecutor** executor)
- 第二段接口:aclnnStatus aclnnBatchNormElemtBackward(void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)
功能描述
-
算子功能:aclnnBatchNormElemt的反向计算。
-
计算公式:

aclnnBatchNormElemtBackwardGetWorkspaceSize
-
接口定义:
aclnnStatus aclnnBatchNormElemtBackwardGetWorkspaceSize(const aclTensor* gradOut, const aclTensor* input, const aclTensor* mean, const aclTensor* invstd, const aclTensor* weight, const aclTensor* meanDy, const aclTensor* meanDyXmu, aclTensor* gradInput,uint64_t* workspaceSize, aclOpExecutor** executor)
-
参数说明:
- gradOut:Device侧的aclTensor,数据类型支持FLOAT16、FLOAT,支持非连续Tensor,数据格式五维及以下支持NCDHW、NCHW、NCL、NC, 六维到八维支持ND。
- input:Device侧的aclTensor,数据类型支持FLOAT16、FLOAT,支持非连续Tensor,数据格式五维及以下支持NCDHW、NCHW、NCL、NC, 六维到八维支持ND。
- mean:Device侧的aclTensor,数据类型仅支持FLOAT16、FLOAT,数据格式为ND。
- invstd:Device侧的aclTensor,数据类型仅支持FLOAT16、FLOAT,数据格式为ND。
- weight:可选参数,Device侧的aclTensor,数据类型仅支持FLOAT16、FLOAT,数据格式为ND。
- meanDy:Device侧的aclTensor,数据类型仅支持FLOAT16、FLOAT,数据格式为ND。
- meanDyXmu:Device侧的aclTensor,数据类型仅支持FLOAT16、FLOAT,数据格式为ND。
- gradInput:Device侧的aclTensor,数据类型与input一致,支持非连续Tensor,数据格式与输入一致。
- workspaceSize:返回用户需要在Device侧申请的workspace大小。
- executor:返回op执行器,包含了算子计算流程。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
:::note 说明 第一段接口完成入参校验,出现以下场景时报错:
- 返回161001(ACLNN_ERR_PARAM_NULLPTR):传入的指针类型入参是空指针。
- 返回161002(ACLNN_ERR_PARAM_INVALID):
- input、output数据类型和数据格式不在支持的范围内。
- input、output数据的shape不在支持的范围内。 :::
aclnnBatchNormElemtBackward
-
接口定义:
aclnnStatus aclnnBatchNormElemtBackward(void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)
-
参数说明:
- workspace:在Device侧申请的workspace内存起址。
- workspaceSize:在Device侧申请的workspace大小,由第一段接口aclnnBatchNormElemtBackwardGetWorkspaceSize获取。
- executor:op执行器,包含了算子计算流程。
- stream:指定执行任务的AscendCL stream流。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
调用示例
<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> 61<br> 62<br> 63<br> 64<br> 65<br> 66<br> 67<br> 68<br> 69<br> 70<br> 71<br> 72<br> 73<br> 74<br> 75<br> 76<br> 77<br> 78<br> 79<br> 80<br> 81<br> 82<br> 83<br> 84<br> 85<br> 86<br> 87<br> 88<br> 89<br> 90<br> 91<br> 92<br> 93<br> 94<br> 95<br> 96<br> 97<br> 98<br> 99<br>100<br>101<br>102<br>103<br>104<br>105<br>106<br>107<br>108<br>109<br>110<br>111<br>112<br>113<br>114<br>115<br>116<br>117<br>118<br>119<br>120<br>121<br>122<br>123<br>124<br>125<br>126<br>127<br>128<br>129<br>130<br>131<br>132<br>133<br>134<br>135<br>136<br>137<br>138<br>139<br>140<br>141<br>142<br>143<br>144<br>145<br>146<br>147<br>148<br>149<br>150<br>151<br>152<br>153<br>154<br>155<br>156<br>157<br>158<br>159<br>160<br>161<br>162<br>163<br>164<br>165<br>166<br>167<br> | cpp<br>#include <iostream><br>#include <vector><br>#include "acl/acl.h"<br>#include "aclnnop/aclnn_batch_norm_elemt_backward.h"<br>#define CHECK_RET(cond, return_expr) \<br> do { \<br> if (!(cond)) { \<br> return_expr; \<br> } \<br> } while (0)<br>#define LOG_PRINT(message, ...) \<br> do { \<br> printf(message, ##__VA_ARGS__); \<br> } while (0)<br>int64_t GetShapeSize(const std::vector<int64_t>& shape) {<br> int64_t shape_size = 1;<br> for (auto i : shape) {<br> shape_size *= i;<br> }<br> return shape_size;<br>}<br>int Init(int32_t deviceId, aclrtContext* context, aclrtStream* stream) {<br> // 固定写法,AscendCL初始化<br> auto ret = aclInit(nullptr);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetDevice(deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateContext(context, deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetCurrentContext(*context);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetCurrentContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret);<br> return 0;<br>}<br>template <typename T><br>int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr,<br> aclDataType dataType, aclTensor** tensor) {<br> auto size = GetShapeSize(shape) * sizeof(T);<br> // 调用aclrtMalloc申请device侧内存<br> auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret);<br> // 调用aclrtMemcpy将Host侧数据拷贝到device侧内存上<br> ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret);<br> // 计算连续tensor的strides<br> std::vector<int64_t> strides(shape.size(), 1);<br> for (int64_t i = shape.size() - 2; i >= 0; i--) {<br> strides[i] = shape[i + 1] * strides[i + 1];<br> }<br> // 调用aclCreateTensor接口创建aclTensor<br> *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND,<br> shape.data(), shape.size(), *deviceAddr);<br> return 0;<br>}<br>int main() {<br> // 1. (固定写法)device/context/stream初始化, 参考AscendCL对外接口列表<br> // 根据自己的实际device填写deviceId<br> int32_t deviceId = 0;<br> aclrtContext context;<br> aclrtStream stream;<br> auto ret = Init(deviceId, &context, &stream);<br> // check根据自己的需要处理<br> CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret);<br> // 2. 构造输入与输出,需要根据API的接口自定义构造<br> std::vector<int64_t> inputShape = {4, 2};<br> std::vector<int64_t> gradOutShape = {4, 2};<br> std::vector<int64_t> gradInputShape = {4, 2};<br> std::vector<int64_t> meanShape = {2};<br> std::vector<int64_t> invstdShape = {2};<br> std::vector<int64_t> weightShape = {2};<br> std::vector<int64_t> meanDyShape = {2};<br> std::vector<int64_t> meanDyXmuShape = {2};<br> void* inputDeviceAddr = nullptr;<br> void* gradOutDeviceAddr = nullptr;<br> void* gradInputDeviceAddr = nullptr;<br> void* meanDeviceAddr = nullptr;<br> void* invstdDeviceAddr = nullptr;<br> void* weightDeviceAddr = nullptr;<br> void* meanDyDeviceAddr = nullptr;<br> void* meanDyXmuDeviceAddr = nullptr;<br> aclTensor* input = nullptr;<br> aclTensor* gradOut = nullptr;<br> aclTensor* gradInput = nullptr;<br> aclTensor* mean = nullptr;<br> aclTensor* invstd = nullptr;<br> aclTensor* weight = nullptr;<br> aclTensor* meanDy = nullptr;<br> aclTensor* meanDyXmu = nullptr;<br> std::vector<float> inputHostData = {0, 1, 2, 3, 4, 5, 6, 7};<br> std::vector<float> gradOutHostData = {1, 1, 1, 2, 2, 2, 3, 3};<br> std::vector<float> gradInputHostData = {0, 0, 0, 0, 0, 0, 0, 0};<br> std::vector<float> meanHostData = {1, 1};<br> std::vector<float> invstdHostData = {1, 1};<br> std::vector<float> weightHostData = {1, 1};<br> std::vector<float> meanDyHostData = {1, 1};<br> std::vector<float> meanDyXmuHostData = {1, 1};<br> ret = CreateAclTensor(inputHostData, inputShape, &inputDeviceAddr, aclDataType::ACL_FLOAT, &input);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(gradOutHostData, gradOutShape, &gradOutDeviceAddr, aclDataType::ACL_FLOAT, &gradOut);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(gradInputHostData, gradInputShape, &gradInputDeviceAddr, aclDataType::ACL_FLOAT, &gradInput);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(meanHostData, meanShape, &meanDeviceAddr, aclDataType::ACL_FLOAT, &mean);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(invstdHostData, invstdShape, &invstdDeviceAddr, aclDataType::ACL_FLOAT, &invstd);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(weightHostData, weightShape, &weightDeviceAddr, aclDataType::ACL_FLOAT, &weight);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(meanDyHostData, meanDyShape, &meanDyDeviceAddr, aclDataType::ACL_FLOAT, &meanDy);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(meanDyXmuHostData, meanDyXmuShape, &meanDyXmuDeviceAddr, aclDataType::ACL_FLOAT, &meanDyXmu);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 3.调用CANN算子库API,需要修改为具体的算子接口<br> uint64_t workspaceSize = 0;<br> aclOpExecutor* executor;<br> // 调用aclnnBatchNormElemtBackward第一段接口<br> ret = aclnnBatchNormElemtBackwardGetWorkspaceSize(gradOut, input, mean, invstd, weight, meanDy, meanDyXmu, gradInput, &workspaceSize, &executor);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormElemtBackwardGetWorkspaceSize failed. ERROR: %d\n", ret); return ret);<br> // 根据第一段接口计算出的workspaceSize申请device内存<br> void* workspaceAddr = nullptr;<br> if (workspaceSize > 0) {<br> ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret;);<br> }<br> // 调用aclnnBatchNormElemtBackward第二段接口<br> ret = aclnnBatchNormElemtBackward(workspaceAddr, workspaceSize, executor, stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormElemtBackward failed. ERROR: %d\n", ret); return ret);<br> // 4. (固定写法)同步等待任务执行结束<br> ret = aclrtSynchronizeStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret);<br> // 5. 获取输出的值,将device侧内存上的结果拷贝至Host侧,需要根据具体API的接口定义修改<br> auto size = GetShapeSize(gradInputShape);<br> std::vector<float> resultData(size, 0);<br> ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), gradInputDeviceAddr, size * sizeof(float),<br> ACL_MEMCPY_DEVICE_TO_HOST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret);<br> for (int64_t i = 0; i < size; i++) {<br> LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]);<br> }<br> // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改<br> aclDestroyTensor(input);<br> aclDestroyTensor(gradOut);<br> aclDestroyTensor(gradInput);<br> aclDestroyTensor(mean);<br> aclDestroyTensor(invstd);<br> aclDestroyTensor(weight);<br> aclDestroyTensor(meanDy);<br> aclDestroyTensor(meanDyXmu);<br> return 0;<br>}<br> |
|---|
父主题: NN类算子接口
aclnnBatchNormGatherStatsWithCounts
接口原型
每个算子有两段接口,必须先调用“aclnnXxxGetWorkspaceSize”接口获取入参并根据计算流程计算所需workspace大小,再调用“aclnnXxx”接口执行计算。两段式接口如下:
- **第一段接口:**aclnnStatus aclnnBatchNormGatherStatsWithCountsGetWorkspaceSize(const aclTensor* input, const aclTensor* mean, const aclTensor* invstd, aclTensor* runningMean, aclTensor* runningVar, double momentum, double eps, const aclTensor* counts, aclTensor* meanAllOut, aclTensor* invstdAllOut, uint64_t* workspaceSize, aclOpExecutor** executor)
- 第二段接口:aclnnStatus aclnnBatchNormGatherStatsWithCounts(void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)
功能描述
-
算子功能:多卡BatchNorm场景下实现SyncBatchNorm,需通过aclnnBatchNormStats、aclnnBatchNormGatherStatsWithCounts和aclnnBatchNormElemt算子组合实现。其中aclnnBatchNormGatherStatsWithCounts主要收集所有device的均值和方差,并更新全局的均值和方差,计算时依赖aclnnBatchNormStats计算单卡数据的均值和标准差的倒数。
:::note 说明 BatchNorm的性能和BatchSize相关,BatchSize越大,BatchNorm的统计量也会越准。对于目标检测类似的任务,占用显存较高,一张显卡往往只能使用较少的图片(比如2张)来训练,这就导致BN的表现变差。为解决该问题,需要实现SyncBatchNorm,即所有卡共享同一个BN,得到全局的统计量。 :::
-
计算公式:
runningMean(M)与runningVar(V)更新公式如下:
aclnnBatchNormGatherStatsWithCountsGetWorkspaceSize
-
接口定义:
aclnnStatus aclnnBatchNormGatherStatsWithCountsGetWorkspaceSize(const aclTensor* input, const aclTensor* mean, const aclTensor* invstd, aclTensor* runningMean, aclTensor* runningVar, double momentum, double eps, const aclTensor* counts, aclTensor* meanAllOut, aclTensor* invstdAllOut, uint64_t* workspaceSize, aclOpExecutor** executor)
-
参数说明:
- input:Device侧的aclTensor,输入Tensor,数据类型支持FLOAT16、FLOAT,支持非连续Tensor,数据格式五维及以下支持NCDHW、NCHW、NCL、NC,六维到八维支持ND。
- mean:输入数据均值,Device侧的aclTensor,数据类型支持FLOAT16、FLOAT,支持非连续的Tensor,数据格式为ND。二维Tensor,第一轴对应的大小与input入参中的C轴长度相同。
- invstd:输入数据标准差的倒数,Device侧的aclTensor,数据类型仅支持FLOAT16、FLOAT,支持非连续的Tensor,数据格式为ND。二维Tensor,第一轴对应的大小与input入参中的C轴长度相同。
- runningMean:训练时数据的均值,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。一维Tensor,长度与input入参中的C轴长度相同。
- runningVar:训练时数据的方差,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。一维Tensor,长度与input入参中的C轴长度相同。
- momentum:runningMean和runningVar的指数平滑参数,默认0.1。
- eps:用于防止做BN时,分母出现0的情况,默认值为1e-5。
- counts:输入数据的元素个数,Device侧的aclTensor,数据类型仅支持FLOAT,支持非连续的Tensor,数据格式为ND。一维Tensor,长度与mean或invstd入参0轴的长度相同。
- meanAll:SyncBatchNorm后,所有卡上数据的均值,Device侧的aclTensor,数据类型仅支持FLOAT16、FLOAT,支持非连续的Tensor,数据格式为ND。
- invstdAll:SyncBatchNorm后,所有卡上数据的标准差的倒数,Device侧的aclTensor,数据类型仅支持FLOAT16、FLOAT,支持非连续的Tensor,数据格式为ND。
- workspaceSize:返回用户需要在Device侧申请的workspace大小。
- executor:返回op执行器,包含了算子计算流程。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
:::note 说明 第一段接口完成入参校验,出现以下场景时报错:
- 返回161001(ACLNN_ERR_PARAM_NULLPTR):传入的指针类型入参是空指针。
- 返回161002(ACLNN_ERR_PARAM_INVALID):
- input、mean、invstd、runningMean、runningVar、counts数据类型和数据格式不在支持的范围内。
- input、mean、invstd、runningMean、runningVar、counts的shape不在支持的范围内。 :::
aclnnBatchNormGatherStatsWithCounts
-
接口定义:
- aclnnStatus aclnnBatchNormGatherStatsWithCounts(void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)
-
参数说明:
- workspace:在Device侧申请的workspace内存起址。
- workspaceSize:在Device侧申请的workspace大小,由第一段接口aclnnBatchNormGatherStatsWithCountsGetWorkspaceSize获取。
- executor:op执行器,包含了算子计算流程。
- stream:指定执行任务的AscendCL stream流。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
调用示例
<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> 61<br> 62<br> 63<br> 64<br> 65<br> 66<br> 67<br> 68<br> 69<br> 70<br> 71<br> 72<br> 73<br> 74<br> 75<br> 76<br> 77<br> 78<br> 79<br> 80<br> 81<br> 82<br> 83<br> 84<br> 85<br> 86<br> 87<br> 88<br> 89<br> 90<br> 91<br> 92<br> 93<br> 94<br> 95<br> 96<br> 97<br> 98<br> 99<br>100<br>101<br>102<br>103<br>104<br>105<br>106<br>107<br>108<br>109<br>110<br>111<br>112<br>113<br>114<br>115<br>116<br>117<br>118<br>119<br>120<br>121<br>122<br>123<br>124<br>125<br>126<br>127<br>128<br>129<br>130<br>131<br>132<br>133<br>134<br>135<br>136<br>137<br>138<br>139<br>140<br>141<br>142<br>143<br>144<br>145<br>146<br>147<br>148<br>149<br>150<br>151<br>152<br>153<br>154<br>155<br> | cpp<br>#include <iostream><br>#include <vector><br>#include "acl/acl.h"<br>#include "aclnnop/aclnn_batch_norm_gather_stats_with_counts.h"<br>#define CHECK_RET(cond, return_expr) \<br> do { \<br> if (!(cond)) { \<br> return_expr; \<br> } \<br> } while (0)<br>#define LOG_PRINT(message, ...) \<br> do { \<br> printf(message, ##__VA_ARGS__); \<br> } while (0)<br>int64_t GetShapeSize(const std::vector<int64_t>& shape) {<br> int64_t shape_size = 1;<br> for (auto i : shape) {<br> shape_size *= i;<br> }<br> return shape_size;<br>}<br>int Init(int32_t deviceId, aclrtContext* context, aclrtStream* stream) {<br> // 固定写法,AscendCL初始化<br> auto ret = aclInit(nullptr);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetDevice(deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateContext(context, deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetCurrentContext(*context);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetCurrentContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret);<br> return 0;<br>}<br>template <typename T><br>int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr,<br> aclDataType dataType, aclTensor** tensor) {<br> auto size = GetShapeSize(shape) * sizeof(T);<br> // 调用aclrtMalloc申请device侧内存<br> auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret);<br> // 调用aclrtMemcpy将Host侧数据拷贝到device侧内存上<br> ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret);<br> // 计算连续tensor的strides<br> std::vector<int64_t> strides(shape.size(), 1);<br> for (int64_t i = shape.size() - 2; i >= 0; i--) {<br> strides[i] = shape[i + 1] * strides[i + 1];<br> }<br> // 调用aclCreateTensor接口创建aclTensor<br> *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND,<br> shape.data(), shape.size(), *deviceAddr);<br> return 0;<br>}<br>int main() {<br> // 1. (固定写法)device/context/stream初始化, 参考AscendCL对外接口列表<br> // 根据自己的实际device填写deviceId<br> int32_t deviceId = 0;<br> aclrtContext context;<br> aclrtStream stream;<br> auto ret = Init(deviceId, &context, &stream);<br> // check根据自己的需要处理<br> CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret);<br> // 2. 构造输入与输出,需要根据API的接口自定义构造<br> std::vector<int64_t> inputShape = {2, 4, 2};<br> std::vector<int64_t> meanShape = {2, 4};<br> std::vector<int64_t> invstdShape = {2, 4};<br> std::vector<int64_t> countShape = {2};<br> std::vector<int64_t> meanOutShape = {4};<br> std::vector<int64_t> invstdOutShape = {4};<br> double eps = 1e-2;<br> void* inputDeviceAddr = nullptr;<br> void* meanDeviceAddr = nullptr;<br> void* invstdDeviceAddr = nullptr;<br> void* countDeviceAddr = nullptr;<br> void* meanOutDeviceAddr = nullptr;<br> void* invstdOutDeviceAddr = nullptr;<br> aclTensor* input = nullptr;<br> aclTensor* mean = nullptr;<br> aclTensor* invstd = nullptr;<br> aclTensor* count = nullptr;<br> aclTensor* meanOut = nullptr;<br> aclTensor* invstdOut = nullptr;<br> std::vector<float> inputHostData = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15};<br> std::vector<float> meanHostData = {1, 2, 3, 4, 5, 6, 7, 8};<br> std::vector<float> invstdHostData = {5, 6, 7, 8, 9, 10, 11, 12};<br> std::vector<float> countHostData = {1, 2};<br> std::vector<float> meanOutHostData = {0, 0, 0, 0}; <br> std::vector<float> invstdOutHostData = {0, 0, 0, 0}; <br> // 创建input aclTensor<br> ret = CreateAclTensor(inputHostData, inputShape, &inputDeviceAddr, aclDataType::ACL_FLOAT, &input);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建mean aclTensor<br> ret = CreateAclTensor(meanHostData, meanShape, &meanDeviceAddr, aclDataType::ACL_FLOAT, &mean);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建invstd aclTensor<br> ret = CreateAclTensor(invstdHostData, invstdShape, &invstdDeviceAddr, aclDataType::ACL_FLOAT, &invstd);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建invstd aclTensor<br> ret = CreateAclTensor(countHostData, countShape, &countDeviceAddr, aclDataType::ACL_FLOAT, &count);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建meanOut aclTensor<br> ret = CreateAclTensor(meanOutHostData, meanOutShape, &meanOutDeviceAddr, aclDataType::ACL_FLOAT, &meanOut);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 创建invstdOut aclTensor<br> ret = CreateAclTensor(invstdOutHostData, invstdOutShape, &invstdOutDeviceAddr, aclDataType::ACL_FLOAT, &invstdOut);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 3.调用CANN算子库API,需要修改为具体的算子接口<br> uint64_t workspaceSize = 0;<br> aclOpExecutor* executor;<br> // 调用aclnnBatchNormGatherStatsWithCounts第一段接口<br> ret = aclnnBatchNormGatherStatsWithCountsGetWorkspaceSize(input, mean, invstd, nullptr, nullptr, 1e-4, 1e-2, count, meanOut, invstdOut, &workspaceSize, &executor);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormGatherStatsWithCountsGetWorkspaceSize failed. ERROR: %d\n", ret); return ret);<br> // 根据第一段接口计算出的workspaceSize申请device内存<br> void* workspaceAddr = nullptr;<br> if (workspaceSize > 0) {<br> ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret;);<br> }<br> // 调用aclnnBatchNormGatherStatsWithCounts第二段接口<br> ret = aclnnBatchNormGatherStatsWithCounts(workspaceAddr, workspaceSize, executor, stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormGatherStatsWithCounts failed. ERROR: %d\n", ret); return ret);<br> // 4. (固定写法)同步等待任务执行结束<br> ret = aclrtSynchronizeStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret);<br> // 5. 获取输出的值,将device侧内存上的结果拷贝至Host侧,需要根据具体API的接口定义修改<br> auto size = GetShapeSize(meanOutShape);<br> std::vector<float> resultData(size, 0);<br> ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), meanOutDeviceAddr, size * sizeof(float),<br> ACL_MEMCPY_DEVICE_TO_HOST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret);<br> for (int64_t i = 0; i < size; i++) {<br> LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]);<br> }<br> // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改<br> aclDestroyTensor(input);<br> aclDestroyTensor(mean);<br> aclDestroyTensor(invstd);<br> aclDestroyTensor(count);<br> aclDestroyTensor(meanOut);<br> aclDestroyTensor(invstdOut);<br> return 0;<br>}<br> |
|---|
父主题: NN类算子接口
aclnnBatchNormReduceBackward
接口原型
每个算子有两段接口,必须先调用“aclnnXxxGetWorkspaceSize”接口获取入参并根据计算流程计算所需workspace大小,再调用“aclnnXxx”接口执行计算。两段式接口如下:
- 第一段接口:aclnnStatus aclnnBatchNormReduceBackwardGetWorkspaceSize(const aclTensor* gradOut, const aclTensor* input, const aclTensor* mean, const aclTensor* invstd, const aclTensor* weight, const bool inputG, const bool weightG, const bool biasG, aclTensor* sumDy, aclTensor* sumDyXmu, aclTensor* gradWeight, aclTensor* gradBias, uint64_t* workspaceSize, aclOpExecutor** executor)
- 第二段接口:aclnnStatus aclnnBatchNormReduceBackward(void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)
功能描述
-
算子功能:BN的反向传播计算了损失函数L相对于对应层各输入xi 的梯度(∂L/∂xi)、对缩放权重γ的梯度(∂L/∂γ)以及对偏移量β的梯度(∂L/∂β),具体参见aclnnBatchNormBackward。本算子计算了其中的“∂L/∂γ”和“∂L/∂β”,并且还用损失函数L相对于输出yi 的偏差推导出“∂L/∂xi”所需的中间量sumDy和sumDyXmu。
-
计算公式:

aclnnBatchNormReduceBackwardGetWorkspaceSize
-
接口定义:
aclnnStatus aclnnBatchNormReduceBackwardGetWorkspaceSize(const aclTensor* gradOut, const aclTensor* input, const aclTensor* mean, const aclTensor* invstd, const aclTensor* weight, const bool inputG, const bool weightG, const bool biasG, aclTensor* sumDy, aclTensor* sumDyXmu, aclTensor* gradWeight, aclTensor* gradBias, uint64_t* workspaceSize, aclOpExecutor** executor)
-
参数说明:
- gradOut:Device侧的aclTensor,数据类型仅支持FLOAT、FLOAT16,支持非连续的Tensor,数据格式为ND。
- input:Device侧的aclTensor,数据类型仅支持FLOAT、FLOAT16,支持非连续的Tensor,数据格式为ND。
- mean:Device侧的aclTensor,数据类型仅支持FLOAT、FLOAT16,支持非连续的Tensor,数据格式为ND。
- invstd:Device侧的aclTensor,数据类型仅支持FFLOAT、FLOAT16,支持非连续的Tensor,数据格式为ND。
- weight:Device侧的aclTensor,数据类型仅支持FLOAT、FLOAT16,支持非连续的Tensor,数据格式为ND。
- inputG:BOOL类型,输出掩码,标记是否需要输出sumDy和sumDyXmu。
- weightG:BOOL类型,输出掩码,标记是否需要输出gradWeight。
- biasG:BOOL类型,输出掩码,标记是否需要输出gradBias。
- sumDy:Device侧的aclTensor,如果inputG为True则输出,支持非连续的Tensor,数据格式与输入一致。
- sumDyXmu:Device侧的aclTensor,如果inputG为True则输出,支持非连续的Tensor,数据格式与输入一致。
- gradWeight:Device侧的aclTensor,如果weightG为True则输出,支持非连续的Tensor,数据格式与输入一致。
- gradBias:Device侧的aclTensor,如果biasG为True则输出,支持非连续的Tensor,数据格式与输入一致。
- workspaceSize:返回用户需要在Device侧申请的workspace大小。
- executor:返回op执行器,包含了算子计算流程。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
第一段接口完成入参校验,出现以下场景时报错:
- 返回161001(ACLNN_ERR_PARAM_NULLPTR):传入的张量参数是空指针。
- 返回161002(ACLNN_ERR_PARAM_INVALID):
- gradOut和input的数据类型和数据格式不在支持的范围内。
- gradOut和input数据类型不相同。
aclnnBatchNormReduceBackward
-
接口定义:
aclnnStatus aclnnBatchNormReduceBackward(void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)
-
参数说明:
- workspace:在Device侧申请的workspace内存起址。
- workspaceSize:在Device侧申请的workspace大小,由第一段接口aclnnBatchNormReduceBackwardGetWorkspaceSize获取。
- executor:op执行器,包含了算子计算流程。
- stream:指定执行任务的AscendCL stream流。
-
返回值:
返回aclnnStatus状态码,具体参见aclnn返回码。
调用示例
<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> 61<br> 62<br> 63<br> 64<br> 65<br> 66<br> 67<br> 68<br> 69<br> 70<br> 71<br> 72<br> 73<br> 74<br> 75<br> 76<br> 77<br> 78<br> 79<br> 80<br> 81<br> 82<br> 83<br> 84<br> 85<br> 86<br> 87<br> 88<br> 89<br> 90<br> 91<br> 92<br> 93<br> 94<br> 95<br> 96<br> 97<br> 98<br> 99<br>100<br>101<br>102<br>103<br>104<br>105<br>106<br>107<br>108<br>109<br>110<br>111<br>112<br>113<br>114<br>115<br>116<br>117<br>118<br>119<br>120<br>121<br>122<br>123<br>124<br>125<br>126<br>127<br>128<br>129<br>130<br>131<br>132<br>133<br>134<br>135<br>136<br>137<br>138<br>139<br>140<br>141<br>142<br>143<br>144<br>145<br>146<br>147<br>148<br>149<br>150<br>151<br>152<br>153<br>154<br>155<br>156<br>157<br>158<br>159<br>160<br>161<br>162<br>163<br>164<br>165<br>166<br>167<br>168<br>169<br>170<br>171<br>172<br>173<br>174<br>175<br>176<br>177<br>178<br>179<br>180<br>181<br>182<br>183<br>184<br>185<br>186<br>187<br>188<br>189<br>190<br>191<br>192<br>193<br>194<br>195<br> | cpp<br>#include <iostream><br>#include <vector><br>#include "acl/acl.h"<br>#include "aclnnop/aclnn_batch_norm_backward_reduce.h"<br>#define CHECK_RET(cond, return_expr) \<br> do { \<br> if (!(cond)) { \<br> return_expr; \<br> } \<br> } while (0)<br>#define LOG_PRINT(message, ...) \<br> do { \<br> printf(message, ##__VA_ARGS__); \<br> } while (0)<br>int64_t GetShapeSize(const std::vector<int64_t>& shape) {<br> int64_t shape_size = 1;<br> for (auto i : shape) {<br> shape_size *= i;<br> }<br> return shape_size;<br>}<br>void PrintOutResult(std::vector<int64_t> &shape, void** deviceAddr) {<br> auto size = GetShapeSize(shape);<br> std::vector<float> resultData(size, 0);<br> auto ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]),<br> *deviceAddr, size * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return);<br> for (int64_t i = 0; i < size; i++) {<br> LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]);<br> }<br>}<br>int Init(int32_t deviceId, aclrtContext* context, aclrtStream* stream) {<br> // 固定写法,AscendCL初始化<br> auto ret = aclInit(nullptr);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetDevice(deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateContext(context, deviceId);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtSetCurrentContext(*context);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetCurrentContext failed. ERROR: %d\n", ret); return ret);<br> ret = aclrtCreateStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret);<br> return 0;<br>}<br>template <typename T><br>int CreateAclTensor(const std::vector<T>& hostData,<br> const std::vector<int64_t>& shape,<br> void** deviceAddr,<br> aclDataType dataType,<br> aclTensor** tensor) {<br> auto size = GetShapeSize(shape) * sizeof(T);<br> // 调用aclrtMalloc申请device侧内存<br> auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret);<br> // 调用aclrtMemcpy将Host侧数据拷贝到device侧内存上<br> ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret);<br> // 计算连续tensor的strides<br> std::vector<int64_t> strides(shape.size(), 1);<br> for (int64_t i = shape.size() - 2; i >= 0; i--) {<br> strides[i] = shape[i + 1] * strides[i + 1];<br> }<br> // 调用aclCreateTensor接口创建aclTensor<br> *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND,<br> shape.data(), shape.size(), *deviceAddr);<br> return 0;<br>}<br>int main() {<br> // 1. (固定写法)device/context/stream初始化, 参考AscendCL对外接口列表<br> // 根据自己的实际device填写deviceId<br> int32_t deviceId = 0;<br> aclrtContext context;<br> aclrtStream stream;<br> auto ret = Init(deviceId, &context, &stream);<br> CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // check根据自己的需要处理<br> // 2. 构造输入与输出,需要根据API的接口自定义构造<br> std::vector<int64_t> gradOutShape = {4, 2};<br> std::vector<int64_t> inputShape = {4, 2};<br> std::vector<int64_t> meanShape = {2};<br> std::vector<int64_t> invstdShape = {2};<br> std::vector<int64_t> weightShape = {2};<br> std::vector<int64_t> sumDyShape = {2};<br> std::vector<int64_t> sumDyXmuShape = {2};<br> std::vector<int64_t> gradWeightShape = {2};<br> std::vector<int64_t> gradBiasShape = {2};<br> void* inputDeviceAddr = nullptr;<br> void* gradOutDeviceAddr = nullptr;<br> void* meanDeviceAddr = nullptr;<br> void* invstdDeviceAddr = nullptr;<br> void* weightDeviceAddr = nullptr;<br> void* sumDyDeviceAddr = nullptr;<br> void* sumDyXmuDeviceAddr = nullptr;<br> void* gradWeightDeviceAddr = nullptr;<br> void* gradBiasDeviceAddr = nullptr;<br> aclTensor* input = nullptr;<br> aclTensor* gradOut = nullptr;<br> aclTensor* mean = nullptr;<br> aclTensor* invstd = nullptr;<br> aclTensor* weight = nullptr;<br> aclTensor* sumDy = nullptr;<br> aclTensor* sumDyXmu = nullptr;<br> aclTensor* gradWeight = nullptr;<br> aclTensor* gradBias = nullptr;<br> std::vector<float> gradOutHostData = {1, 1, 1, 2, 2, 2, 3, 3};<br> std::vector<float> inputHostData = {0, 1, 2, 3, 4, 5, 6, 7};<br> std::vector<float> meanHostData = {1, 1};<br> std::vector<float> invstdHostData = {1, 1};<br> std::vector<float> weightHostData = {1, 1};<br> std::vector<float> sumDyHostData = {1, 1};<br> std::vector<float> sumDyXmuHostData = {1, 1};<br> std::vector<float> gradWeightHostData = {1, 1};<br> std::vector<float> gradBiasHostData = {1, 1};<br> ret = CreateAclTensor(gradOutHostData, gradOutShape, &gradOutDeviceAddr, aclDataType::ACL_FLOAT, &gradOut);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(inputHostData, inputShape, &inputDeviceAddr, aclDataType::ACL_FLOAT, &input);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(meanHostData, meanShape, &meanDeviceAddr, aclDataType::ACL_FLOAT, &mean);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(invstdHostData, invstdShape, &invstdDeviceAddr, aclDataType::ACL_FLOAT, &invstd);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(weightHostData, weightShape, &weightDeviceAddr, aclDataType::ACL_FLOAT, &weight);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> bool inputG = true;<br> bool weightG = true;<br> bool biasG = true;<br> ret = CreateAclTensor(sumDyHostData, sumDyShape, &sumDyDeviceAddr, aclDataType::ACL_FLOAT, &sumDy);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(sumDyXmuHostData, sumDyXmuShape, &sumDyXmuDeviceAddr, aclDataType::ACL_FLOAT, &sumDyXmu);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(gradWeightHostData, gradWeightShape, &gradWeightDeviceAddr, aclDataType::ACL_FLOAT, &gradWeight);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> ret = CreateAclTensor(gradBiasHostData, gradBiasShape, &gradBiasDeviceAddr, aclDataType::ACL_FLOAT, &gradBias);<br> CHECK_RET(ret == ACL_SUCCESS, return ret);<br> // 3. 调用CANN算子库API<br> uint64_t workspaceSize = 0;<br> aclOpExecutor* executor;<br> // 调用aclnnBatchNormReduceBackward第一段接口<br> ret = aclnnBatchNormReduceBackwardGetWorkspaceSize(gradOut, input, mean, invstd, weight,<br> inputG, weightG, biasG,<br> sumDy, sumDyXmu, gradWeight, gradBias,<br> &workspaceSize, &executor);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormReduceBackwardGetWorkspaceSize failed. ERROR: %d\n", ret); return ret);<br> // 根据第一段接口计算出的workspaceSize申请device内存<br> void* workspaceAddr = nullptr;<br> if (workspaceSize > 0) {<br> ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret;);<br> }<br> // 调用aclnnBatchNormReduceBackwardNpuImpl第二段接口<br> ret = aclnnBatchNormReduceBackward(workspaceAddr, workspaceSize, executor, stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnBatchNormReduceBackward failed. ERROR: %d\n", ret); return ret);<br> // 4. (固定写法)同步等待任务执行结束<br> ret = aclrtSynchronizeStream(stream);<br> CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret);<br> // 5. 获取输出的值,将device侧内存上的结果拷贝至Host侧,需要根据具体API的接口定义修改<br> PrintOutResult(sumDyShape, &sumDyDeviceAddr);<br> PrintOutResult(sumDyXmuShape, &sumDyXmuDeviceAddr);<br> PrintOutResult(gradWeightShape, &gradWeightDeviceAddr);<br> PrintOutResult(gradBiasShape, &gradBiasDeviceAddr);<br> // 6. 释放aclTensor和aclScalar,需要根据具体API的接口定义修改<br> aclDestroyTensor(input);<br> aclDestroyTensor(gradOut);<br> aclDestroyTensor(mean);<br> aclDestroyTensor(invstd);<br> aclDestroyTensor(weight);<br> aclDestroyTensor(sumDy);<br> aclDestroyTensor(sumDyXmu);<br> aclDestroyTensor(gradWeight);<br> aclDestroyTensor(gradBias);<br> return 0;<br>}<br> |
|---|
父主题: NN类算子接口
在线提单