资讯详情

资讯详情

GroupNorm 算子实战:从分组原理到 Ascend 调用全流程

GroupNorm 算子实战从分组原理到 Ascend 调用全流程【免费下载链接】BabelDOCYet Another Document Translator项目地址: https://gitcode.com/GitHub_Trending/ba/BabelDOC把跑在 Ascend 芯片上的 AI 翻译模型如 BabelDOC 这类场景落到 CANN 底层时常常要自己写归一化算子。这篇带你把 GroupNorm 从分组原理到完整调用一次讲透。原理速览按通道分组做归一化 把一批特征图想成一个班级N个样本学生每人手里一张C×H×W的矩阵。GroupNorm 做的事是——先把C个通道切成groupNum份每份内部各自算平均均值 μ和分散程度方差 σ再用组均值、组标准差把这份数据拉平最后乘一个可学习的缩放 γ、加一个可学习的平移 β。用文字把公式写出来就是均值μ (1/m) · Σ xᵢ方差σ sqrt( (1/m) · Σ (xᵢ - μ)² ε )输出y γ · (x - μ) / σ β其中m是参与统计的数值个数ε是个极小常数专门用来防止σ趋近 0 时除零。四个常见 Norm 的差异全在于统计范围圈住了哪些维度算子在 [N, C, H, W] 上圈住的统计维度一句话记忆BatchNorm每个通道[C, H, W]跨N个样本同一通道、整批样本一起算LayerNorm每个样本的[C, H, W]全量一个样本内通道空间全算InstanceNorm每个样本、每个通道[1, H, W]每个通道单独、只看空间GroupNorm每组[C/G, H, W]C切G组通道切组组内算groupNum的两个极端正好退化回其他 Norm取groupNum 1时所有通道并成一组每组统计范围就是整份[C, H, W]等价于 LayerNorm取groupNum C时每个通道自成一组每组只剩[1, H, W]等价于 InstanceNorm。所以 GroupNorm 可以看成在这两个端点之间可调的连续体。接口速查两种原型与一张参数表 两种函数原型区别只在临时空间怎么来第一种接口自己去申请第二种让你用sharedTmpBuffer传进来后者能更省内存。// 原型一接口内部自己申请临时空间 template typename T, bool isReuseSource false __aicore__ inline void GroupNorm(const LocalTensorT output, const LocalTensorT outputMean, const LocalTensorT outputVariance, const LocalTensorT inputX, const LocalTensorT gamma, const LocalTensorT beta, const T epsilon, GroupNormTiling tiling); // 原型二通过 sharedTmpBuffer 传入临时空间 template typename T, bool isReuseSource false __aicore__ inline void GroupNorm(const LocalTensorT output, const LocalTensorT outputMean, const LocalTensorT outputVariance, const LocalTensorT inputX, const LocalTensorT gamma, const LocalTensorT beta, const LocalTensoruint8_t sharedTmpBuffer, const T epsilon, GroupNormTiling tiling);参数速查表参数名方向类型shape一句话说明output输出LocalTensorT[N,C,H,W]归一化后再做 γ 缩放、β 平移的最终结果outputMean输出LocalTensorT[N,G]每组算出的均值常留给反向传播outputVariance输出LocalTensorT[N,G]每组算出的方差同上inputX输入LocalTensorT[N,C,H,W]待归一化的特征图gamma输入LocalTensorT[C]可学习缩放参数取值限 [-100,100]beta输入LocalTensorT[C]可学习平移参数取值限 [-100,100]sharedTmpBuffer输入LocalTensoruint8_t见 Tiling原型二专用接口内部算中间变量用epsilon输入标量 T-防除零系数类型须与 inputX/output 一致tiling输入GroupNormTiling-切分信息用 GetGroupNormNDTillingInfo 填模板参数两个T是操作数数据类型isReuseSource默认false。isReuseSourcetrue 与 false 的差别它决定接口内部算的时候要不要直接改写inputX那块内存。传true复用inputX的空间来存中间结果省下一份内存代价是输入张量会被改写之后不能再当原始数据用。传false不动inputX多占一份内存但输入保持只读。这里注意只有float输入才能开truehalf输入不支持传了会报错。平台与数据类型Ascend 950PR / 950DT、Atlas A3 训练/推理系列、Atlas A2 训练/推理系列都支持数据类型统一是half和float。该算子没有返回值void结果直接落在 output、outputMean、outputVariance 三个张量里。调用示例逐段拆解 // 目标对 ND 格式 [N, C, H, W] 输入做一次 GroupNormC 通道分 G 组 template typename dataType, bool isReuseSource false __aicore__ inline void MainGroupnormTest(GM_ADDR inputXGm, GM_ADDR gammGm, GM_ADDR betaGm, GM_ADDR outputGm, uint32_t n, uint32_t c, uint32_t h, uint32_t w, uint32_t g) { dataType epsilon 0.001; // 防除零类型必须和输入一致 DataFormat dataFormat DataFormat::ND; // 只支持 ND // 绑定全局地址主张量 N*C*H*W 个元素gamma/beta 各 C 个 GlobalTensordataType inputXGlobal; GlobalTensordataType gammGlobal; GlobalTensordataType betaGlobal; GlobalTensordataType outputGlobal; uint32_t bshLength n * c * h * w; uint32_t bsLength g * n; // [N, G] 统计量的元素数 inputXGlobal.SetGlobalBuffer(reinterpret_cast__gm__ dataType*(inputXGm), bshLength); gammGlobal.SetGlobalBuffer(reinterpret_cast__gm__ dataType*(gammGm), c); betaGlobal.SetGlobalBuffer(reinterpret_cast__gm__ dataType*(betaGm), c); outputGlobal.SetGlobalBuffer(reinterpret_cast__gm__ dataType*(outputGm), bshLength); // Pipe 队列3 个输入队列(主/gamma/beta)、1 个输出队列、2 个算均值方差的计算缓冲 TPipe pipe; TQueTPosition::VECIN, 1 inQueueX; TQueTPosition::VECIN, 1 inQueueGamma; TQueTPosition::VECIN, 1 inQueueBeta; TQueTPosition::VECOUT, 1 outQueue; TBufTPosition::VECCALC meanBuffer, varBuffer; // 对齐H*W 向上取整到 ONE_BLK_SIZE得到 hwAlignSize供 DataCopyPad 补尾 uint32_t hwAlignSize (sizeof(dataType) * h * w ONE_BLK_SIZE - 1) / ONE_BLK_SIZE * ONE_BLK_SIZE / sizeof(dataType); pipe.InitBuffer(inQueueX, 1, sizeof(dataType) * n * c * hwAlignSize); pipe.InitBuffer(inQueueGamma, 1, (sizeof(dataType) * c 31) / 32 * 32); // 32B 对齐 pipe.InitBuffer(inQueueBeta, 1, (sizeof(dataType) * c 31) / 32 * 32); pipe.InitBuffer(outQueue, 1, sizeof(dataType) * n * c * hwAlignSize); pipe.InitBuffer(meanBuffer, (sizeof(dataType) * g * n 31) / 32 * 32); // [N, G] pipe.InitBuffer(varBuffer, (sizeof(dataType) * g * n 31) / 32 * 32); // 申请 LocalTensor LocalTensordataType inputXLocal inQueueX.AllocTensordataType(); LocalTensordataType gammaLocal inQueueGamma.AllocTensordataType(); LocalTensordataType betaLocal inQueueBeta.AllocTensordataType(); LocalTensordataType outputLocal outQueue.AllocTensordataType(); LocalTensordataType meanLocal meanBuffer.GetdataType(); LocalTensordataType varianceLocal varBuffer.GetdataType(); // 拷入局部内存主张量每行补 (hwAlignSize - H*W)gamma/beta 原样拷 DataCopyParams copyParams{static_castuint16_t(n*c), static_castuint16_t(h*w*sizeof(dataType)), 0, 0}; DataCopyPadParams padParams{true, 0, static_castuint8_t(hwAlignSize - h * w), 0}; DataCopyPad(inputXLocal, inputXGlobal, copyParams, padParams); DataCopyParams copyParamsGamma{1, static_castuint16_t(c*sizeof(dataType)), 0, 0}; DataCopyPadParams padParamsGamma{false, 0, 0, 0}; DataCopyPad(gammaLocal, gammGlobal, copyParamsGamma, padParamsGamma); DataCopyPad(betaLocal, betaGlobal, copyParamsGamma, padParamsGamma); PipeBarrierPIPE_ALL(); // 等拷贝完成再进入计算 // 查询算子可用的栈空间自申请临时空间的原型靠它 uint32_t stackBufferSize 0; { LocalTensorfloat stackBuffer; bool ans PopStackBufferfloat, TPosition::LCM(stackBuffer); stackBufferSize stackBuffer.GetSize(); } // 取 Tiling填 shape 与组数 g GroupNormTiling groupNormTiling; uint32_t inputShape[4] {n, c, h, w}; ShapeInfo shapeInfo{ (uint8_t)4, inputShape, (uint8_t)4, inputShape, dataFormat }; GetGroupNormNDTillingInfo(shapeInfo, stackBufferSize, sizeof(dataType), isReuseSource, g, groupNormTiling); // 调算子此处演示自申请版若想省栈空间可改走 sharedTmpBuffer 版 GroupNormdataType, isReuseSource(outputLocal, meanLocal, varianceLocal, inputXLocal, gammaLocal, betaLocal, (dataType)epsilon, groupNormTiling); PipeBarrierPIPE_ALL(); // 写回并释放 DataCopyPad(outputGlobal, outputLocal, copyParams); inQueueX.FreeTensor(inputXLocal); inQueueGamma.FreeTensor(gammaLocal); inQueueBeta.FreeTensor(betaLocal); outQueue.FreeTensor(outputLocal); PipeBarrierPIPE_ALL(); }张量与队列初始化先让输入输出在管线上跑起来所以用TPipe加几个队列来管数据流三个TQueVECIN分别喂主张量、gamma、beta一个TQueVECOUT收结果两个TBufVECCALC专门存均值和方差。易错点均值方差是[N, G]形状别按[N, C]去开缓冲。内存分配与对齐计算局部内存大小要提前定好关键是hwAlignSize把H*W向上取整到ONE_BLK_SIZE的整数倍这样主张量每行都按对齐后的宽度排。gamma/beta 则按 32 字节对齐取整(c*sizeof(T)31)/32*32。这里容易踩如果H*W没对齐后面DataCopyPad的补尾长度就对不上。DataCopyPad 拷贝策略数据得先进局部内存才能算所以主张量用DataCopyPad一边拷一边按行补(hwAlignSize - H*W)个空位gamma、beta 不需要补参数里padLength传 0 原样拷。细节copyParams的第一个字段是行数n*c第二个是每行字节数别把h*w和c写反。Tiling 信息获取算之前要先告诉内核怎么切数据这一步用GetGroupNormNDTillingInfo填GroupNormTiling。它需要四元 shape、stackBufferSize用PopStackBuffer查出来的可用栈空间、类型大小、isReuseSource和组数g。这里容易踩走 sharedTmpBuffer 原型时临时空间大小BufferSize也要从 Tiling 文档里取不能自己估。算子调用核心就一行把前面申请的 6 个 LocalTensor 加上 epsilon 和 tiling 喂进去。注意epsilon要显式转成dataType否则类型和输入对不上。调用前后各加一次PipeBarrierPIPE_ALL()保证上一段拷贝/计算完成后再往下走。写回与释放最后把 output 从局部内存写回全局再逐个FreeTensor释放结尾再来一次PipeBarrier收尾。易错点写回用的copyParams和拷入时是同一份行数、行宽保持一致否则写回会对错位。避坑清单 ️这里容易踩DataFormat只能传ND传 NHWC 或其他格式直接不认shape 也按 [N,C,H,W] 来。isReuseSource true只对float输入有效half输入开了会报错——想省内存还得先看数据类型。epsilon类型必须和inputX/output一致half输入别随手传一个float字面量记得强转。gamma/beta有取值范围限制 [-100, 100]别当成无约束的权重乱传。走 sharedTmpBuffer 原型时临时空间大小BufferSize要从 Tiling 文档获取自己拍脑袋算容易开小导致越界。hwAlignSize一定要对齐到ONE_BLK_SIZEDataCopyPad的补尾长度就是拿它减H*W得到的没对齐就全乱了。延伸参考 GroupNorm TilingGroupNormTiling结构怎么填、sharedTmpBuffer的BufferSize怎么取都在这份文档里。通用地址对齐说明操作数地址对齐约束含ONE_BLK_SIZE、32B 对齐规则的出处。LocalTensor / DataCopyPad局部内存张量抽象与带补位的拷贝接口调算子前的内存搬运都靠它俩。把分组逻辑想清楚、对齐和类型这几处盯住这个算子在 Ascend 上就能稳定跑通。【免费下载链接】BabelDOCYet Another Document Translator项目地址: https://gitcode.com/GitHub_Trending/ba/BabelDOC创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →