CANN ops-math 数学算子 Erfc(互补误差函数):aclnnErfc 与 aclnnInplaceErfc 双段式接口使用指南
2026/9/20 5:22:11 网站建设 项目流程

CANN ops-math 数学算子 Erfc(互补误差函数):aclnnErfc 与 aclnnInplaceErfc 双段式接口使用指南

【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math

导读

本文基于 CANN ops-math 开源仓库中 math/erfc/docs/aclnnErfc&aclnnInplaceErfc.md 编写,系统讲解互补误差函数(erfc)算子的aclnnErfcaclnnInplaceErfc两组两段式接口:包括产品支持情况、数学定义、函数原型、入参约束、返回码语义、确定性约束以及完整的可运行示例。读完本文,你将能够在 Ascend NPU 上通过单算子 API 完成 erfc 的前向计算,并能根据是否需要原地(in-place)写回自行选择合适接口。文中所有接口原型、约束与错误码均以 op_api/aclnn_erfc.h 与 op_api/aclnn_erfc.cpp 等仓库源码为准。

1. 算子功能与数学定义

Erfc算子返回输入 Tensor 中每个元素对应的**误差互补函数(Complementary Error Function)**的值,是 CANN ops-math 中数学类基础算子之一(算子源码目录为 math/erfc)。

计算公式为:

$$ \mathrm{erfc}(x)=1-\frac{2}{\sqrt{\pi}}\int_{0}^{x}e^{-t^{2}}\mathrm{d}t $$

从定义可以看出,erfc(x) = 1 - erf(x),它描述了标准正态分布右尾概率的倍数关系,在通信理论(高斯误差函数积分)、数值计算与深度学习激活函数变体中经常出现。与普通误差函数不同,erfc 对较大的正自变量的计算更稳定,因为它直接刻画了尾部的衰减行为。

在仓库的算子注册文件 op_host/erfc_def.cpp 中,该算子在 Ascend 图引擎侧以Erfc名称注册,输入x与输出y支持的数据类型为BF16 / FLOAT16 / FLOAT,数据格式为ND,并开启了动态 rank、动态 shape 支持以及精度缩减(PrecisionReduce)标志。

1.1 产品支持情况

根据文档与 math/erfc/README.md 中的支持矩阵,Erfc 算子在不同产品上的支持情况如下:

产品是否支持
Ascend 950PR / Ascend 950DT支持
Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持
Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持
Atlas 200I/500 A2 推理产品不支持
Atlas 推理系列产品支持
Atlas 训练系列产品支持

注意:BFLOAT16数据类型并非所有产品都支持,例如 Atlas A2 训练/推理系列产品、Atlas 推理系列产品、Atlas 训练系列产品不支持 BFLOAT16(详见 math/erfc/README.md 的“参数说明”一节)。

2. 接口选型:aclnnErfc 与 aclnnInplaceErfc

aclnnErfcaclnnInplaceErfc实现完全相同的数学功能,二者的区别仅在于输出结果的存放方式

  • aclnnErfc:需要显式新建一个输出张量对象(out)来存储计算结果,输入与输出分离,适合需要保留原始输入的场景;
  • aclnnInplaceErfc:无需新建输出张量对象,直接在输入张量(selfRef)的内存中覆盖存储计算结果,适合不需要保留原始输入、希望节省设备内存的场景。

从实现层面看,op_api/aclnn_erfc.cpp 中aclnnInplaceErfcGetWorkspaceSize实际上是把selfRef同时作为输入和输出,复用aclnnErfcGetWorkspaceSize的内部计算流程,因此两者在计算逻辑上完全一致,只是对外暴露的接口形态不同。

与 CANN 其他单算子接口一致,每组接口都遵循两段式调用范式(参见 docs/zh/context/two_phase_api.md):

  1. 第一段接口aclnnErfcGetWorkspaceSize/aclnnInplaceErfcGetWorkspaceSize——完成入参校验,计算执行所需的 workspace 大小,并返回包含算子计算流程的执行器executor
  2. 第二段接口aclnnErfc/aclnnInplaceErfc——传入已申请的 workspace 与 executor,在指定 stream 上真正执行计算。

这种设计把“计算规划”(可能耗时较长)与“计算执行”分离,便于上层框架预先规划内存并复用执行器。

3. 函数原型详解

四个接口的原型声明位于头文件 op_api/aclnn_erfc.h,具体如下。

3.1 aclnnErfcGetWorkspaceSize

aclnnStatus aclnnErfcGetWorkspaceSize( const aclTensor* self, aclTensor* out, uint64_t* workspaceSize, aclOpExecutor** executor)

参数说明:

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
self(aclTensor*)输入待进行 erfc 计算的入参shape 必须和 out 一样DOUBLE、FLOAT32、FLOAT16、BOOL、INT64ND不超过 8 维
out(aclTensor*)输出erfc 计算的出参数据类型默认和 self 保持一致;若 self 为 BOOL 或 INT64 时,out 默认取 FLOAT32;shape 必须和 self 一样DOUBLE、FLOAT32、FLOAT16ND不超过 8 维
workspaceSize(uint64_t*)输出返回需要在 Device 侧申请的 workspace 大小-----
executor(aclOpExecutor**)输出返回 op 执行器,包含了算子计算流程-----

产品差异补充(源自文档与 op_api/aclnn_erfc.cpp 中的支持列表):

  • Ascend 950PR/Ascend 950DT、Atlas A3 训练/推理系列产品self额外支持BFLOAT16out额外支持BFLOAT16
  • 其余产品线(Atlas A2 训练/推理、Atlas 推理、Atlas 训练系列)的 self 与 out 支持 DOUBLE、FLOAT32、FLOAT16,不支持 BFLOAT16。

上述“产品差异”对应源码中的两张支持列表:ASCEND910_DTYPE_DTYPE_SUPPORT_LIST(DOUBLE/FLOAT/FLOAT16/BOOL/INT64)与ASCEND910B_DTYPE_DTYPE_SUPPORT_LIST(在前者基础上增加 BF16),运行时通过GetDtypeSupportListV2根据实际平台选择。

3.2 aclnnErfc

aclnnStatus aclnnErfc( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)

参数说明:

参数名输入/输出描述
workspace输入在 Device 侧申请的 workspace 内存地址
workspaceSize输入在 Device 侧申请的 workspace 大小,由第一段接口 aclnnErfcGetWorkspaceSize 获取
executor输入op 执行器,包含了算子计算流程
stream输入指定执行任务的 Stream

3.3 aclnnInplaceErfcGetWorkspaceSize

aclnnStatus aclnnInplaceErfcGetWorkspaceSize( const aclTensor* selfRef, uint64_t* workspaceSize, aclOpExecutor** executor)

参数说明:

参数名输入/输出描述使用说明数据类型数据格式维度(shape)非连续Tensor
selfRef(aclTensor*)输入/输出输入输出 Tensor-DOUBLE、FLOAT32、FLOAT16ND不超过 8 维
workspaceSize(uint64_t*)输出返回需要在 Device 侧申请的 workspace 大小-----
executor(aclOpExecutor**)输出返回 op 执行器,包含了算子计算流程-----

产品差异补充:

  • Ascend 950PR/Ascend 950DT、Atlas A3 训练/推理系列产品selfRef额外支持BFLOAT16
  • 其余产品线的 selfRef 仅支持 DOUBLE、FLOAT32、FLOAT16。

值得注意的是,inplace 接口的selfRef支持的数据类型范围比非 inplace 接口的self更窄——不支持 BOOL 与 INT64,因为 BOOL/INT64 输入需要先 Cast 到 FLOAT32 再计算,结果无法直接覆盖回原输入的内存。这一点在 op_api/aclnn_erfc.cpp 的ASCEND910_DTYPE_SELFREF_LIST/ASCEND910B_DTYPE_SELFREF_LIST中有明确体现。

3.4 aclnnInplaceErfc

aclnnStatus aclnnInplaceErfc( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream)

参数说明:

参数名输入/输出描述
workspace输入在 Device 侧申请的 workspace 内存地址
workspaceSize输入在 Device 侧申请的 workspace 大小,由第一段接口 aclnnInplaceErfcGetWorkspaceSize 获取
executor输入op 执行器,包含了算子计算流程
stream输入指定执行任务的 Stream

3.5 返回值与错误码

所有接口均返回aclnnStatus状态码,完整返回码定义参见 docs/zh/context/aclnn_return_code.md。

第一段接口(GetWorkspaceSize)会完成入参校验,出现如下场景时返回对应错误码:

aclnnErfcGetWorkspaceSize 的校验逻辑:

返回值错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 self 或 out 是空指针时
ACLNN_ERR_PARAM_INVALID161002self 和 out 的数据类型和数据格式不在支持的范围之内
ACLNN_ERR_PARAM_INVALID161002erfc 计算结果不能 cast 成 out 类型
ACLNN_ERR_PARAM_INVALID161002self 与 out 的 shape 不同
ACLNN_ERR_PARAM_INVALID161002self 和 out 的维度大于 8

aclnnInplaceErfcGetWorkspaceSize 的校验逻辑:

返回值错误码描述
ACLNN_ERR_PARAM_NULLPTR161001传入的 selfRef 是空指针时
ACLNN_ERR_PARAM_INVALID161002selfRef 的数据类型和数据格式不在支持的范围之内
ACLNN_ERR_PARAM_INVALID161002selfRef 的数据维度大于 8

上述校验逻辑与源码一一对应:op_api/aclnn_erfc.cpp 中的CheckParamsErfc依次检查空指针(CheckNotNull2Tensor)、数据类型合法性(CheckDtypeValid,其中 BOOL/INT64 输入时校验结果是否能 cast 成 out 类型)以及 shape 一致性(CheckSameShape1In1Out);CheckInplaceParamsErfc则只校验 selfRef 的空指针与数据类型。

4. 内部计算流程与约束说明

4.1 源码视角的计算图

从 op_api/aclnn_erfc.h 的接口注释可以看到,非 inplace 版本在框架内部被编排为如下计算图:

Self --> l0op::Contiguous --> l0op::Erfc --> l0op::Cast --> l0op::ViewCopy --> out

而当self的数据类型为BOOL 或 INT64时,会先执行一次 Cast 到 FLOAT32,再进入 Erfc 计算:

Self --> l0op::Contiguous --> l0op::Cast(FLOAT32) --> l0op::Erfc --> l0op::Cast --> l0op::ViewCopy --> out

对照 op_api/aclnn_erfc.cpp 的实现,这一流程非常清晰:

  1. self为空 Tensor(IsEmpty()),直接返回workspaceSize = 0,不进入计算;
  2. l0op::Contiguous将非连续输入转为连续 Tensor;
  3. self为 BOOL/INT64,先l0op::Cast到 FLOAT32;
  4. l0op::Erfc执行核心计算;
  5. l0op::Cast将结果转为out声明的数据类型;
  6. out是非连续 Tensor,l0op::ViewCopy将连续结果写回非连续布局。

inplace 版本的计算图相对更短:

Self --> l0op::Contiguous --> l0op::Erfc --> l0op::ViewCopy --> out

4.2 Kernel 层实现

核心 kernel 位于 op_kernel/arch35/erfc.cpp,它根据输入类型在编译期选择两条 DAG 执行路径(见 op_kernel/arch35/erfc_dag.h):

  • FLOAT32(无需 Cast)CopyIn<T> -> Vec::Erfc<float> -> CopyOut<T>
  • FLOAT16 / BFLOAT16(需要精度提升)CopyIn<T> -> Cast<float, T> -> Vec::Erfc<float> -> Cast<T, float>(RINT) -> CopyOut<T>,即先升到 float32 计算、再以舍入模式降回原类型,保证低精度类型下的计算精度。

kernel 使用ElementwiseSch逐元素调度框架,内存优化级别为LEVEL_2,任务类型为KERNEL_TYPE_AIV_ONLY。仓库同时提供了对应的 kernel UT:tests/ut/op_kernel/test_erfc.cpp,覆盖了 float 等类型的逐元素正确性验证。

4.3 约束说明

  • 确定性计算aclnnErfcaclnnInplaceErfc默认即为确定性实现(同一输入多次运行结果一致),无需额外配置。关于确定性计算的更完整说明可参考 docs/zh/context/determinism_compute.md。
  • 维度不超过 8 维、数据格式为 ND、支持非连续 Tensor(相关机制可参考 docs/zh/context/non_contiguous_tensor.md)。

5. 完整调用示例(可运行)

下面的示例代码来自 examples/test_aclnn_erfc.cpp(与文档中的示例同源),在一个main函数中同时演示了aclnnErfcaclnnInplaceErfc两组接口的完整调用流程,输入为 shape{2, 2}的 FLOAT32 Tensor,数据为{0, 1, 2, 3}

#include <iostream> #include <vector> #include "acl/acl.h" #include "aclnnop/aclnn_erfc.h" #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) int64_t GetShapeSize(const std::vector<int64_t>& shape) { int64_t shape_size = 1; for (auto i : shape) { shape_size *= i; } return shape_size; } int Init(int32_t deviceId, aclrtStream* stream) { // 固定写法,资源初始化 auto ret = aclInit(nullptr); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclInit failed. ERROR: %d\n", ret); return ret); ret = aclrtSetDevice(deviceId); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSetDevice failed. ERROR: %d\n", ret); return ret); ret = aclrtCreateStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtCreateStream failed. ERROR: %d\n", ret); return ret); return 0; } template <typename T> int CreateAclTensor(const std::vector<T>& hostData, const std::vector<int64_t>& shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size = GetShapeSize(shape) * sizeof(T); // 调用aclrtMalloc申请device侧内存 auto ret = aclrtMalloc(deviceAddr, size, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMalloc failed. ERROR: %d\n", ret); return ret); // 调用aclrtMemcpy将host侧数据拷贝到device侧内存上 ret = aclrtMemcpy(*deviceAddr, size, hostData.data(), size, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret); // 计算连续tensor的strides std::vector<int64_t> strides(shape.size(), 1); for (int64_t i = shape.size() - 2; i >= 0; i--) { strides[i] = shape[i + 1] * strides[i + 1]; } // 调用aclCreateTensor接口创建aclTensor *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int main() { // 1.(固定写法)device/stream初始化,参考acl API手册 // 根据自己的实际device填写deviceId int32_t deviceId = 0; aclrtStream stream; auto ret = Init(deviceId, &stream); CHECK_RET(ret == 0, LOG_PRINT("Init acl failed. ERROR: %d\n", ret); return ret); // 2. 构造输入与输出,需要根据API的接口自定义构造 std::vector<int64_t> selfShape = {2, 2}; std::vector<int64_t> outShape = {2, 2}; void* selfDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* self = nullptr; aclTensor* out = nullptr; std::vector<float> selfHostData = {0, 1, 2, 3}; std::vector<float> outHostData = {0, 0, 0, 0}; // 创建self aclTensor ret = CreateAclTensor(selfHostData, selfShape, &selfDeviceAddr, aclDataType::ACL_FLOAT, &self); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建out aclTensor ret = CreateAclTensor(outHostData, outShape, &outDeviceAddr, aclDataType::ACL_FLOAT, &out); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用CANN算子库API uint64_t workspaceSize = 0; aclOpExecutor* executor; // 调用aclnnErfc第一段接口 ret = aclnnErfcGetWorkspaceSize(self, out, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnErfcGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* workspaceAddr = nullptr; if (workspaceSize > 0) { ret = aclrtMalloc(&workspaceAddr, workspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret;); } // 调用aclnnErfc第二段接口 ret = aclnnErfc(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnErfc failed. ERROR: %d\n", ret); return ret); uint64_t inplaceWorkspaceSize = 0; aclOpExecutor* inplaceExecutor; // 调用aclnnInplaceErfc第一段接口 ret = aclnnInplaceErfcGetWorkspaceSize(self, &inplaceWorkspaceSize, &inplaceExecutor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceErfcGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 void* inplaceWorkspaceAddr = nullptr; if (inplaceWorkspaceSize > 0) { ret = aclrtMalloc(&inplaceWorkspaceAddr, inplaceWorkspaceSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("allocate workspace failed. ERROR: %d\n", ret); return ret;); } // 调用aclnnInplaceErfc第二段接口 ret = aclnnInplaceErfc(inplaceWorkspaceAddr, inplaceWorkspaceSize, inplaceExecutor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnInplaceErfc failed. ERROR: %d\n", ret); return ret); // 4.(固定写法)同步等待任务执行结束 ret = aclrtSynchronizeStream(stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtSynchronizeStream failed. ERROR: %d\n", ret); return ret); // 5. 获取输出的值,将device侧内存上的结果拷贝至host侧 auto size = GetShapeSize(outShape); std::vector<float> resultData(size, 0); ret = aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, size * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < size; i++) { LOG_PRINT("result[%ld] is: %f\n", i, resultData[i]); } auto inplaceSize = GetShapeSize(selfShape); std::vector<float> inplaceResultData(inplaceSize, 0); ret = aclrtMemcpy(inplaceResultData.data(), inplaceResultData.size() * sizeof(inplaceResultData[0]), selfDeviceAddr, inplaceSize * sizeof(float), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy result from device to host failed. ERROR: %d\n", ret); return ret); for (int64_t i = 0; i < inplaceSize; i++) { LOG_PRINT("inplaceResult[%ld] is: %f\n", i, inplaceResultData[i]); } // 6. 释放aclTensor aclDestroyTensor(self); aclDestroyTensor(out); // 7. 释放device资源 aclrtFree(selfDeviceAddr); aclrtFree(outDeviceAddr); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } if (inplaceWorkspaceSize > 0) { aclrtFree(inplaceWorkspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }

5.1 结果验证

对输入{0, 1, 2, 3}执行 erfc 计算的期望结果为:

erfc(0) = 1.0 erfc(1) ≈ 0.157299 erfc(2) ≈ 0.004678 erfc(3) ≈ 0.000022

上述期望值在 examples/test_aclnn_erfc.cpp 中以 1e-6 的容差参与结果比对;文档示例则在 host 侧打印result[i]inplaceResult[i],两种方式得到的结果完全一致——这也从侧面验证了 inplace 版本与非 inplace 版本计算语义相同。

5.2 编译与运行

  • 示例代码的编译与运行步骤与 CANN 其他 aclnn 单算子样例一致,请参考 docs/zh/context/compile_and_run_sample.md,其中包含环境变量设置、编译链接选项以及执行流程的完整说明。
  • 示例中涉及的基础概念(device/stream 初始化、Tensor 构造、内存申请与拷贝)可参考 docs/zh/context/basic_concept.md 与 docs/zh/context/data_structure.md。
  • 仓库还提供了**图模式(GEIR)**调用方式,见 examples/test_geir_erfc.cpp,通过 op_graph/erfc_proto.h 中的算子 IR 构图调用,适用于将 Erfc 嵌入整图编译的场景。

5.3 示例代码执行流程拆解

对照两段式接口的规范,示例代码实际完成了 7 个固定步骤,任何 aclnn 单算子调用都遵循这一骨架:

  1. 初始化aclInitaclrtSetDeviceaclrtCreateStream
  2. 构造 TensoraclrtMalloc申请 Device 内存 →aclrtMemcpy写入输入数据 → 计算连续 strides →aclCreateTensor创建 aclTensor;
  3. 两段式调用:第一段接口拿workspaceSizeexecutor,按需aclrtMallocworkspace,再调用第二段接口执行;
  4. 同步等待aclrtSynchronizeStream
  5. 取回结果aclrtMemcpy(DEVICE_TO_HOST)并打印/校验;
  6. 释放 TensoraclDestroyTensor
  7. 释放资源aclrtFree内存与 workspace、aclrtDestroyStreamaclrtResetDeviceaclFinalize

注意其中 workspace 的申请是条件性的if (workspaceSize > 0)):当第一段接口返回的 workspace 大小为 0 时无需申请,传入空指针即可。

6. 相关资源索引

  • 接口文档:math/erfc/docs/aclnnErfc&aclnnInplaceErfc.md
  • 算子 README:math/erfc/README.md
  • 接口声明:op_api/aclnn_erfc.h、op_api/aclnn_erfc.cpp
  • 算子注册与 shape 推导:op_host/erfc_def.cpp、op_host/erfc_infershape.cpp
  • Kernel 实现:op_kernel/arch35/erfc.cpp、op_kernel/arch35/erfc_dag.h
  • 示例代码:examples/test_aclnn_erfc.cpp、examples/test_geir_erfc.cpp
  • 测试用例:tests/ut/op_kernel/test_erfc.cpp、tests/ut/op_api/test_aclnn_erfc.cpp、tests/ut/op_host/arch35/test_erfc_tiling.cpp
  • 通用概念:docs/zh/context/two_phase_api.md、docs/zh/context/aclnn_return_code.md、docs/zh/context/compile_and_run_sample.md、docs/zh/context/determinism_compute.md

【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询