【昇腾CANN训练营】激活函数GELU算子开发学习
GELU简介
当我们谈论神经网络中的激活函数时,可能会听到一些专业名词,比如Sigmoid、ReLU,或者GELU。这些名字听起来或许有些高深,但它们的核心思想其实可以用浅显的道理来解释。
早期的神经网络常用Sigmoid函数作为激活函数。它的曲线平滑,输出范围在0到1之间,非常适合模拟概率。但问题是,Sigmoid在输入值较大或较小时,梯度会变得非常小,导致训练速度极慢,这就是所谓的“梯度消失”问题。
为了解决这个问题,ReLU(Rectified Linear Unit)登场了。它的规则很简单:输入大于零时直接输出,小于零时输出为零。这种设计让梯度在正区间内保持稳定,大大加快了训练速度。但ReLU也有缺点,比如“神经元死亡”问题——一旦某些神经元的输入为负,它们可能永远无法被激活。
于是,研究者们开始寻找更优的解决方案,GELU(Gaussian Error Linear Unit)便是其中之一。GELU结合了ReLU的简洁性和概率分布的平滑性。它的核心思想是:让激活值不仅依赖于输入的正负,还依赖于输入的概率分布。简单来说,GELU会根据输入的大小,以一定的概率“决定”是否激活。这种设计既保留了ReLU的高效性,又避免了神经元死亡的问题。
GELU算子开发学习
首先要找到CANN-OPS算子仓中的GELU算子源码,开发者通常会注意到它在 cann-ops/src/math/gelu
目录下。math
目录不仅包含了GELU算子的实现,还存放着其他常用神经网络的算子。但问题是,为什么我们要从这个算子开始学习?相信专家会有更系统的训练方法,只是框架原理一类的概念并不是我们当前的重点。本文只是希望用尽量清晰的路径,帮助理解看似复杂的算子开发流程。
图-目录结构
接下来我们可以从学习顺序上梳理一下,如何高效掌握GELU算子的实现。在算子仓中,每个算子都会附带详细的说明文档,为开发者带来关键的知识养分。第一层目录下的 README.md
就是那个最显眼的入口,而其他的说明文档则藏在更下层的目录中。学习GELU算子的最佳顺序可以分四步走:
-
阅读文档:先看
README.md
,了解算子的功能、输入输出和约束条件,就像在动手前先看说明书。 -
掌握调用方式:学习
ACLNN
的接口定义,搞清楚如何在框架中正确使用这个算子。在昇腾论坛的算子开发教学课程中,很多课程会先讲解算子实现,再学习调用方式。但这样的顺序就像让人先研究发动机原理,再学怎么开车——也许对初学者并不友好。这里我们采用"先用再究"的方式,试图能让复杂的技术概念更快变得清晰可触。
完整的调用流程,本文不打算展开详述,这里只聚焦两个最关键的操作节点,也就是课程里常说的二段式调用:获取工作空间大小和调用GELU算子的执行接口。
//获取工作空间大小 ret = aclnnGeluGetWorkspaceSize(inputX, outputZ, &workspaceSize, &executor); // 执行算子 ret = aclnnGelu(workspaceAddr, workspaceSize, executor, stream); 复制
-
深入实现细节:拆解
host
端(CPU侧逻辑)和kernel
端(NPU侧计算)的代码,理解数据流和计算逻辑。在GELU算子的Host端接口实现中,最关键的语句是分片计算函数 TilingFunc 和注册部分。
// 设置分片参数 tiling.set_totalLength(totalLength); tiling.set_ALIGN_NUM(ALIGN_NUM); tiling.set_tiling_size(tiling_size); tiling.set_block_size(block_size); tiling.set_aivNum(aivNum); tiling.set_core_size(core_size); tiling.set_core_remain(core_remain); // 设置计算核心数 context->SetBlockDim(aivNum); //注册 this->AICore().SetTiling(optiling::TilingFunc); 复制
在GELU算子的Kernel端计算逻辑中,计算核心浓缩在
Compute()
方法里,其调用Ascend C算子基础API进行运算。图-GELU表达式
//计算前加入输入tensor对float32的转换,计算后转回原类型 if constexpr ( ! std::is_same_v<T, float32_t>) { LocalTensor<float> p1 = tmp1.Get<float>(); LocalTensor<float> p2 = tmp2.Get<float>(); Cast(p1, srcLocal, RoundMode::CAST_NONE, length); Cast(p2, srcLocal, RoundMode::CAST_NONE, length); Mul(p2, p1, p1, length); Mul(p2, p2, p1, length); Muls(p2, p2, (float)this->param1, length); Add(p2, p2, p1, length); Muls(p2, p2, (float)this->param2, length); Exp(p2,p2,length); Adds(p2, p2, (float)1, length); Div(p2,p1,p2,length); Cast(dstLocal, p2, RoundMode::CAST_RINT, length); tmp1.FreeTensor(p1); tmp2.FreeTensor(p2); } else { Mul(dstLocal, srcLocal, srcLocal, length); Mul(dstLocal, dstLocal, srcLocal, length); Muls(dstLocal, dstLocal, (T)this->param1, length); Add(dstLocal, dstLocal, srcLocal, length); Muls(dstLocal, dstLocal, (T)this->param2, length); Exp(dstLocal,dstLocal,length); Adds(dstLocal, dstLocal, (T)1, length); Div(dstLocal,srcLocal,dstLocal,length); } 复制
-
验证与测试:通过
ST(System Test)
测试用例,确保算子的正确性符合预期。在GELU算子的系统测试用例中,最关键的语句是计算GELU函数的预期结果数据的核心逻辑。
golden = x / (1 + np.exp(-1.595769122 * (x + 0.0455399241 * x**3)))
- 点赞
- 收藏
- 关注作者
评论(0)