GridUnnormal 算子深度解析:CANN ops-cv 中 GridSample 链路的坐标反归一化实现与 GE 图模式调用
【免费下载链接】ops-cv本项目是CANN提供的图像处理、目标检测相关的算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-cv
导读
GridUnnormal 是 CANN ops-cv 开源算子库中服务于 GridSample 链路的坐标反归一化(unnormalize)算子:它以归一化采样坐标grid与同形状的尺寸辅助张量assist为输入,逐元素输出pos_base的小数偏移diff与整数采样位置position,为后续双线性/双三次采样的取点与插值提供基础。本文以 image/grid_unnormal/README.md 为骨架,结合算子原型、Infershape、Tiling、Kernel 与测试代码,系统讲解其计算公式、参数约束、产品支持范围、源码级实现原理以及 GE IR 图模式调用样例,帮助开发者理解并正确使用这一 GE 图内部算子。
产品支持情况
GridUnnormal 算子在不同硬件产品上的支持情况如下(以仓库 README 为准):
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR/Ascend 950DT | √ |
| Atlas A3 训练系列产品/Atlas A3 推理系列产品 | √ |
| Atlas A2 训练系列产品/Atlas A2 推理系列产品 | √ |
| Atlas 200I/500 A2 推理产品 | × |
| Atlas 推理系列产品 | × |
| Atlas 训练系列产品 | √ |
从源码结构看,算子 Kernel 与 Tiling 均位于arch35目录(op_kernel/arch35、op_host/arch35),对应 DAV_3510 架构(即 Ascend 950 系列)的寄存器编程实现,这与 README 中 Ascend 950 系列“√”的支持状态一致。
功能说明:GridSample 链路中的坐标反归一化
算子定位
GridUnnormal 是 GridSample 链路中的坐标反归一化算子,处理的是"把归一化采样坐标还原为输入图像上的真实浮点采样位置"这一环节。其输入包含两部分:
grid:归一化采样坐标,元素取值通常落在[-1, 1]区间;assist:与grid同 shape 的尺寸辅助张量,每元素对应输入在某维上的尺寸值(例如图像的高或宽)。
算子输出两个结果:pos_base的小数部分diff,以及pos_base的下取整结果position(int32 整数采样位置),两者可直接作为后续插值采样的基础。
计算公式
对每个元素独立计算:
t = (grid + 1) * 0.5 pos_base = align_corners ? t * (assist - 1) : t * assist - 0.5 position = floor(pos_base) diff = pos_base - floor(pos_base)其中floor按向负无穷取整语义执行,不能用向零截断(trunc)替代。这一约束在 Kernel 实现中有直接体现:grid_unnormal.h 中定义了kCastF32ToI32Floor,其RoundMode显式指定为CAST_FLOOR,保证负数坐标按 floor 语义下取整。
两种align_corners模式分别对应 GridSample 语义中的两种坐标对齐约定:
align_corners = true:输入输出张量角点像素中心对齐,坐标映射为t * (assist - 1);align_corners = false(默认):不做角点对齐,坐标映射为t * assist - 0.5。
数值验证(golden 实现)
golden.py 提供了两套参考实现用于交叉验证:_grid_unnormal_golden_compute采用与 README 等价的代数变形(pos_base = ((grid + 1) * assist - 1) / 2),_grid_unnormal_third_party_compute则严格按 README 公式逐字实现(pos_base = normalized * assist - 0.5),并通过 torch 的torch.floor计算下取整结果。测试容差配置中,diff输出使用cross_check标准(float32/float16 均为 L1 级别),position输出使用binary_equal(整数逐位相等)标准。
参数说明
| 参数名 | 输入/输出/属性 | 描述 | 数据类型 | 数据格式 |
|---|---|---|---|---|
grid | 输入 | 归一化采样坐标。支持 4D 静态 shape、动态 shape 和编译期未知 rank;rank 确定时必须为 4 且末维为 2;支持总元素数为 0 的空 Tensor。 | float16、float32 | ND |
assist | 输入 | 每元素对应的输入尺寸辅助值。必须与gridshape、dtype 完全一致,不支持广播;rank 确定时必须为 4 且末维为 2。 | float16、float32 | ND |
diff | 输出 | pos_base的小数部分,shape 与grid一致,dtype 与grid一致。 | float16、float32 | ND |
position | 输出 | pos_base的下取整结果,shape 与grid一致。 | int32 | ND |
align_corners | 属性 | 可选属性,默认false。为true时按t * (assist - 1)计算;为false时按t * assist - 0.5计算。 | bool | - |
上述参数定义在算子原型 grid_unnormal_proto.h 中通过REG_OP(GridUnnormal)声明,与 grid_unnormal_def.cpp 中的 OpDef 注册完全一致:输入输出均为REQUIRED,align_corners为OPTIONAL且默认false。dtype 组合由 2 行 dtype 表描述:grid/assist/diff = {fp16, fp32},position = {int32, int32},即 diff 跟随 grid 的 dtype,position 固定为 int32。
约束说明
grid与assist的 shape、dtype 必须一致,不支持广播;- rank 确定时,
grid与assist必须为 4D Tensor,shape 为[batch, height, width, 2](末维 2 表示坐标分量); - 仅支持 ND 格式;非连续 Tensor 作为用户可见接口不涉及;
diff的 dtype 跟随grid;position固定为 int32;- 中间计算使用 fp32;fp16 输入会提升到 fp32 计算,
diff再回写为 fp16; - 总元素数为 0 的空 Tensor 支持空进空出,设备侧不访问数据;
- 非有限输入(NaN/Inf)以及
floor(pos_base)超出 int32 表示范围不属于本算子支持域; - 本算子为 GE 图内部算子,不提供 aclnn、torch、TensorFlow、ONNX、Caffe 对外接口。
这些约束在 Infershape 与 Tiling 代码中均有对应检查:grid_unnormal_infershape.cpp 会校验 rank 必须为 4、末维必须为 2(未知维度除外)、grid 与 assist 的已知维度必须逐维相等;grid_unnormal_tiling_arch35.cpp 在编译期进一步校验存储 shape 完全一致且两个输入 dtype 必须相等(L150-L156)。golden.py 的注释也明确指出:该 OpDef 为 aclnn_exclude,未交付 torch_npu 绑定,也未交付 TensorFlow/ONNX 解析器与融合 pass。
动态 shape 支持细节
- 编译期未知 rank 通过
-2标记表示:Infershape 中IsUnknownRank检查shape->GetDimNum() == 1 && shape->GetDim(0) == -2时直接放行; - 动态 shape 场景下,末维
-1(UNKNOWN_DIM)被允许(只要不等于 2 之外的其他已知值),Tiling 阶段再基于实际存储 shape 计算总元素数; - 算子声明中
DynamicRankSupportFlag(true)、DynamicShapeSupportFlag(true)、DynamicCompileStaticFlag(true)(grid_unnormal_def.cpp)确认了上述能力的编译期开启状态。
调用说明:GE IR 图模式调用
GridUnnormal 作为 GE 图内部算子,通过构图方式在图中使用。README 给出的调用方式为:
| 调用方式 | 调用样例 | 说明 |
|---|---|---|
| 图模式调用 | test_geir_grid_unnormal | 通过本目录的算子原型构图方式调用 GridUnnormal 算子。 |
调用样例核心流程
test_geir_grid_unnormal.cpp 演示了完整的 GE IR 图模式调用流程,关键步骤包括:
- 创建算子节点:
op::GridUnnormal("gridUnnormal_1"),基于 grid_unnormal_proto.h 生成的op::GridUnnormal类; - 构造输入占位节点:
op::Data("grid").set_attr_index(0)与op::Data("assist").set_attr_index(1),通过update_input_desc_x声明 FORMAT_ND、DT_FLOAT 的 4D 描述; - 绑定输入:
gridUnnormal.set_input_grid(grid)、gridUnnormal.set_input_assist(assist); - 设置属性:
gridUnnormal.set_attr_align_corners(false); - 声明输出描述:
update_output_desc_diff(DT_FLOAT)与update_output_desc_position(DT_INT32); - 构图执行:
graph.SetInputs(inputs).SetOutputs(outputs)后,经Session->AddGraph(graphId, graph)与Session->RunGraph(graphId, input, output)完成执行,其中全局配置ge.exec.deviceId=0、ge.graphRunMode=1。
样例数值推演
样例配置为align_corners = false,grid与assist均为DT_FLOAT、shape[1, 6, 5, 2],grid 每元素填0.3f、assist 每元素填5.0f,期望输出:
t = (0.3 + 1) * 0.5 = 0.65 pos_base = 0.65 * 5 - 0.5 = 2.75 position = floor(2.75) = 2 (int32) diff = 2.75 - 2 = 0.75即diff输出全为 0.75(float),position输出全为 2(int32)。这个手算推演与 golden.py 的 torch 参考实现可以相互印证,可直接作为自测基准。
源码实现原理
Kernel 实现:RegBase 寄存器编程范式
grid_unnormal.cpp 是 kernel 入口,采用 ops-cv 非模板extern "C"约定:dtype 由DTYPE_GRID编译期实例化(fp16/fp32 各一份),align_corners走 tilingdata 运行时分支。入口通过REGISTER_TILING_DEFAULT注册默认 Tiling,随后调用NsGridUnnormal::GridUnnormalKernel<DTYPE_GRID>完成初始化与计算。
grid_unnormal.h 中GridUnnormalKernel<T>的实现要点:
- 数据搬运:GM↔UB 使用
TQue+DataCopyPad(CopyIn/CopyOut),每个核心按coreStart_/coreLen_处理属于自己的元素区间,双缓冲(kBufNum = 2)隐藏搬运与计算延迟; - 向量计算:在
__VEC_SCOPE__内用 MicroAPI 寄存器算子(RegTensor/MaskReg/Adds/Muls/Mul/Sub/Cast)完成逐元素计算。t = (grid + 1) * 0.5通过Adds加 1 再Muls乘 0.5 实现;align_corners为 true 时先Adds(aReg, -1)再Mul,为 false 时先Mul再Adds(-0.5)(模板参数AlignCorners编译期展开); - 统一 fp32 中间计算:fp16 输入由
LoadOneTensorForDtypeT载入即升为 fp32,StoreOneTensorForDtypeT存回时再降为 fp16——与 README"中间计算使用 fp32"的约束对应; - floor 语义保证:
position使用Cast<int32_t, float, kCastF32ToI32Floor>(RoundMode 为CAST_FLOOR)保证向负无穷下取整,随后通过Cast<float, int32_t>还原浮点 floor 值做Sub得到diff; - 寄存器位宽:向量寄存器 256B,fp32 通道每拍 64 元素(
kFp32PerLoop = 64),repeatTimes按 64 元素对齐循环。
Tiling 实现:扁平化分核 + UB 切分
grid_unnormal_tiling_arch35.cpp 实现 host 侧 Tiling,策略为"纯 elementwise,按总元素数扁平分核":
- 通过
PlatformAscendC获取 AIV 核数coreNum与 UB 大小ubSize(GetPlatformInfo); - 校验存储 shape 与 dtype 后计算总元素数
total(GetInputInfo); - 依据 UB 容量与每元素字节数(
BytesPerElem:两个输入 + diff 输出按 dtype 字节计、position 按 int32 计,均乘双缓冲数 2)计算ubFactor,并向下对齐到 64 元素整数倍(CalcUbFactor),预留 8KB UB 余量; - 按
perCore = ceil(total / coreNum)向上取整均分到各核,usedCores收窄为实际需要的核数并设置SetBlockDim(FillNormalTiling); - 空 Tensor(
total <= 0)走 FillEmptyTiling:totalNum=0、blockDim=1、workspace 为 0,实现"空进空出、设备侧不访问数据"。
TilingData 结构定义在 grid_unnormal_tiling_data.h:totalNum(总元素数)、perCoreNum(每核元素数)、ubFactor(单次 UB tile 元素数)、alignCorners(0/1 分支标志)。
算子配置与编译产物
op_host/config/ascend950/grid_unnormal_binary.json 描述了 ascend950 平台预编译产物的规格:为 fp16 与 fp32 两种 dtype 各生成一份二进制(GridUnnormal_1d9cf9915315ae9f8e867b978473f984与GridUnnormal_f7372211154d6f1cad01274230b0fdf9),输入输出 shape 均为-2(编译期未知 rank,动态 shape 入口),align_corners属性在编译产物中值为 null(运行时决定)。同目录下的grid_unnormal_simplified_key.ini提供简化 key 配置。
测试验证
仓库为 GridUnnormal 提供了 host 侧与 kernel 侧两层单元测试:
- Infershape/Tiling 单测:test_grid_unnormal_infershape.cpp 验证形状与 dtype 推导逻辑;test_grid_unnormal_tiling.cpp 验证 Tiling 数据与分核结果;
- Kernel 单测:test_grid_unnormal.cpp 配合 gen_data.py 生成输入数据、compare_data.py 与 golden 结果比对,验证设备侧实际计算结果;
- golden 参考:golden.py 提供 torch 参考实现,float32/float16 输出按 L1 交叉校验、int32 输出按逐位相等校验。
总结
GridUnnormal 作为 GridSample 链路的坐标反归一化算子,以"两输入(grid/assist)、两输出(diff/position)、一个可选属性(align_corners)"的简洁接口,在 Ascend 950(arch35)平台上以统一 fp32 中间精度、CAST_FLOOR下取整语义、扁平化分核 Tiling 与 RegBase 寄存器编程实现了高效且行为可预期的坐标还原。理解其计算公式与约束(4D、末维为 2、shape/dtype 一致、ND 格式、floor 语义)是正确使用的前提;如需在图中集成该算子,可参考 test_geir_grid_unnormal.cpp 的 GE IR 构图方式,并通过 golden.py 的参考实现做数值验证。
【免费下载链接】ops-cv本项目是CANN提供的图像处理、目标检测相关的算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-cv
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考