已关闭
[Requirement|需求建议]: LpNormV2算子AscendC实现贡献 #218
石向阳创建于 2025年12月8日关闭于 5月12日
石向阳
2025年12月8日 评论:
2025年12月8日 评论:
/assign


2025年12月8日 将 shi-xiangyang225 设为负责人
2025年12月8日 关联了pull request:个人-AscendC实现LpNormV2算子贡献
2025年12月15日 修改了issue 的描述
2025年12月25日 添加了label:Accepted
4月30日 修改了issue 的描述
5月12日 关闭了 issue
5月12日 添加了label:resolved
Thanks for sending an requirement! Please fill in the following template to help quickly solve your problem.
Backgroud(背景信息)
使用AscendC对LpNormV2算子进行实现,实现了AscendC实现的LpNormV2算子对Atlas 200/500 A2推理产品和Atlas 800I/T A2硬件的适配。
Origin(信息来源)
哈工大算子团队
Benefit / Necessity (价值/作用)
LpNormV2 算子的主要功能是对输入张量在指定维度(dim)上计算 Lp 次范数并归一化,支持配置范数阶数 p(如 p=1、p=2、p=∞ 等),支持配置轴(默认为-1即求全局范数并规约,支持axis=0,=1,分别按照行和列归约)常用于深度学习模型的特征归一化(如防止梯度爆炸 / 消失、统一特征尺度)、距离度量(如 L2 范数用于欧氏距离计算)、注意力机制的权重规范化、生成模型与分类模型的正则化约束、计算机视觉的特征提取后处理、自然语言处理的词向量标准化等场景。
目前该算子支持 float / float16 数据类型,支持维度为 1–2D Tensor
Design(设计方案)
一、总体设计目标
LpNormV3 算子用于在 Ascend C 平台上实现通用 Lp 范数归一化计算,支持 axis = -1 / 0 / 1 以及 p 为有限值、正无穷、负无穷三类情形。整体设计目标如下:
二、Host 端设计方案(LpNormV3TilingFunc)
2.x Host 端核心流程伪代码
// Host side tiling entry Status LpNormV3TilingFunc(Context &ctx) { // 1. 获取平台信息 PlatformInfo plat = GetPlatformInfo(ctx); uint32_t coreNum = plat.aivCoreNum; uint32_t ubSize = plat.ubSize; // 2. 解析输入 shape / 属性 Shape xShape = ctx.GetInputShape(0); // [rows, cols] int axis = ctx.GetAttr("axis"); // -1 / 0 / 1 float p = ctx.GetAttr("p"); // 3. 计算总数据量 int64_t totalDataNum = xShape.rows * xShape.cols; // 4. core 任务均分 CoreSplit split = SplitTask(totalDataNum, coreNum); // split.bigCoreNum / split.smallCoreNum // 5. tile 切分(基于 UB) TileInfo tile; tile.tileDataNum = CalcTileDataNum(ubSize, sizeof(dtype)); tile.tileNum = split.coreDataNum / tile.tileDataNum; tile.tailDataNum = split.coreDataNum % tile.tileDataNum; // 6. workspace 规划(按 axis 分配) int64_t workspaceSize = CalcWorkspaceSize(axis, xShape); ctx.SetWorkspaceSize(workspaceSize); // 7. 设置 tiling 数据 TilingData tiling; tiling.axis = axis; tiling.p = p; tiling.coreDataNum = split.coreDataNum; tiling.tileDataNum = tile.tileDataNum; tiling.tileNum = tile.tileNum; tiling.tailDataNum = tile.tailDataNum; ctx.SetTilingData(tiling); // 8. tilingKey(仅区分 dtype / 模板) ctx.SetTilingKey(CalcTilingKey(dtype)); return SUCCESS; }Host 端主要负责 平台感知、数据切分、tiling 参数生成以及 kernel 分支选择,整体流程如下:
2.1 平台信息获取
通过
GetPlatformInfo获取:若核心数或 UB 大小非法,直接返回失败,保证后续计算安全。
2.2 输入 Shape 与属性解析
从
context中获取:统一将输入视为 二维张量:
2.3 CoreNum 与任务均分策略
设计原则:优先满核,其次均分
根据 totalDataNum 与 coreNum 计算:
bigCoreDataNum / smallCoreDataNumtailBlockNum(需要多处理一个 tile 的核心数)若数据量无法整除:
tailBlockNum个核心2.4 Tile 切分策略
根据 UB 大小、BUFFER_NUM、dtype size 计算:
tileDataNum:单次搬运的数据量tileNum:每个 core 的 tile 次数tailDataNum:最后一个 tile 的实际数据量Tile 设计目标:
2.5 Workspace 规划
Workspace 用于 多核范数归约结果的全局同步:
设计原因:
2.6 TilingKey 规划策略
不基于 shape / axis 划分 tilingKey
tilingKey 主要用于:
范数类型(普通 / ±∞)不通过 tilingKey 区分:
该策略降低了 kernel 数量,提升代码复用率。
三、Kernel 端设计方案
3.x Kernel 端整体控制流伪代码
// Kernel entry for each core __aicore__ void LpNormV3Kernel(TilingData tiling) { Init(tiling); if (!IsInfNorm(tiling.p)) { NormalProcess(tiling); } else { InfProcess(tiling); } }Kernel 端采用 Init + Process 的两阶段模型。
四、Init 阶段详细设计
4.x Init 阶段伪代码
__aicore__ void Init(TilingData tiling) { // 1. 计算当前 core 的数据范围 coreId = GetCoreId(); globalOffset = CalcGlobalOffset(coreId, tiling); // 2. 绑定 global memory xGm = GlobalTensor(xAddr + globalOffset); yGm = GlobalTensor(yAddr + globalOffset); workGm= GlobalTensor(workAddr); // 3. 初始化 pipe / queue(双缓冲) pipe.Init(); inQueue.Init(); outQueue.Init(); // 4. 初始化 localSum(axis 相关) if (axis == -1) { localSum = 0; } else if (axis == 0) { localSum[rows] = 0; } else { // axis == 1 localSum[cols] = 0; } }4.1 Global Buffer 绑定
根据 tilingData 计算当前核心:
globalBufferIndexcoreDataNum绑定:
xGm:输入数据yGm:输出结果workGm:跨核共享 workspace(严格按 axis 分配)4.2 Local Buffer 与 Pipe 初始化
使用 双缓冲(BUFFER_NUM = 2):
计算缓冲区:
poweredBuf:存储 |x|^p 或 |x|tmp_norm:归一化所需范数tmp_base:原子累加与中间计算4.3 localSum 初始化(axis 相关)
axis = -1:
axis = 0:
axis = 1:
初始化策略:
五、Process 阶段设计
5.1 NormalProcess(p 为有限值)
NormalProcess 伪代码
__aicore__ void NormalProcess(TilingData tiling) { // Reduce 阶段 for (int t = 0; t < tiling.tileNum; ++t) { CopyIn(xGm, inQueue, t); ComputeReduce(inQueue, localSum, tiling.axis, tiling.p); } if (tiling.tailDataNum > 0) { CopyInTail(xGm, inQueue); ComputeReduce(inQueue, localSum, tiling.axis, tiling.p); } // 多核归约与同步 AtomicAdd(workGm, localSum); SyncAll(); // 核 0 计算最终范数 if (GetCoreId() == 0) { norm = Pow(workGm + epsilon, 1.0f / tiling.p); WriteBack(norm); CacheCleanInvalid(workGm); } SyncAll(); // Normalization 阶段 for (int t = 0; t < tiling.tileNum; ++t) { CopyIn(xGm, inQueue, t); CopyIn(norm, tmp_norm); ComputeDiv(inQueue, tmp_norm, outQueue); CopyOut(outQueue, yGm, t); } }5.1.1 Reduce 阶段
CopyIn:tile 数据搬入 UB
Reduce:
5.1.2 SumAndSyncAll 阶段
各核心通过 AtomicAdd 将 localSum 写入 workGm
使用:
SyncAll + Barrier确保所有核写入完成核 0:
(sum + epsilon)^(1/p)5.1.3 Normalization 阶段
5.2 InfProcess(p = ±∞)
InfProcess 伪代码
__aicore__ void InfProcess(TilingData tiling) { // ReduceInf 阶段 for (int t = 0; t < tiling.tileNum; ++t) { CopyIn(xGm, inQueue, t); ComputeReduceInf(inQueue, localSum, tiling.axis, tiling.p); } // 多核 min / max 归约 if (tiling.p > 0) { AtomicMax(workGm, localSum); } else { AtomicMin(workGm, localSum); } SyncAll(); // 核 0 写回最终范数 if (GetCoreId() == 0) { WriteBack(workGm); CacheCleanInvalid(workGm); } SyncAll(); // Normalization for (int t = 0; t < tiling.tileNum; ++t) { CopyIn(xGm, inQueue, t); CopyIn(workGm, tmp_norm); ComputeDiv(inQueue, tmp_norm, outQueue); CopyOut(outQueue, yGm, t); } }与 NormalProcess 结构一致,差异点在于:
ReduceInf:
SumAndSyncAllInf:
AtomicMax / AtomicMin六、同步与一致性设计
Atomic 操作:
Barrier + SyncAll:
Cache Clean & Invalid:
七、设计总结
Host 端:
Kernel 端: