CANN ops-math:aclnnCalculateMatmulWeightSizeV2 接口详解——Matmul 权重 NZ 格式转换空间计算与实战

CANN ops-math:aclnnCalculateMatmulWeightSizeV2 接口详解——Matmul 权重 NZ 格式转换空间计算与实战 CANN ops-mathaclnnCalculateMatmulWeightSizeV2 接口详解——Matmul 权重 NZ 格式转换空间计算与实战【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-mathaclnnCalculateMatmulWeightSizeV2是 CANN ops-math 库中 trans_data 模块提供的一个轻量级辅助接口当 Matmul 类算子以 ND 格式输入权重时它用于计算权重转换为 NZFractal_NZ格式所需占用的空间大小以元素个数计帮助开发者在预处理权重缓冲区时精确申请内存从而让 Matmul 算子获得最优执行性能。阅读本文后你将掌握该接口的功能定位、计算公式、参数约束、返回码含义以及如何结合aclnnTransMatmulWeight完成先算容量、再转格式的完整权重预处理流程。产品支持情况该接口的产品支持情况如下摘自 aclnnCalculateMatmulWeightSizeV2.md产品是否支持Ascend 950PR / Ascend 950DT支持Atlas A3 训练系列产品 / Atlas A3 推理系列产品支持Atlas A2 训练系列产品 / Atlas A2 推理系列产品支持Atlas 200I / 500 A2 推理产品不支持Atlas 推理系列产品支持Atlas 训练系列产品不支持从源码结构看aclnn_trans_matmul_weight.cpp 中的CheckSocValid()会检查当前 NPU 架构仅当架构为DAV_2201Ascend 910B 系列、DAV_2002Atlas 310P 系列或寄存器基IsRegBase架构时返回成功否则抛出ACLNN_ERR_RUNTIME_ERROR361001产品型号不支持与上表的不支持清单相互印证。功能说明与计算公式接口的功能定位是在 Matmul 算子 ND 格式输入下计算如果要转换到 NZ 格式下需要占用的空间大小单位为元素个数。该接口仅用于判断对 weight Tensor 预处理时应该申请多大的缓冲区才能使 Matmul 算子执行性能最优。文档给出两个典型示例输入[510, 510]数据类型 Float16/Bfloat16出于性能角度考虑函数会将 shape 变化为[512, 512]因此返回的容量为262144即 512 × 512。输入[510, 270]数据类型 INT8函数会将 shape 变化为[512, 288]因此返回的容量为147456即 512 × 288。计算公式按数据类型区分Float16/Bfloat16: result Align(Shapesize[0], 16) * Align(Shapesize[1], 16) INT8: result Align(Shapesize[0], 16) * Align(Shapesize[1], 32)其中Align(x, n)表示向上对齐到n的整数倍。对齐粒度的物理含义是 NPU Cube 单元处理分块矩阵时的最小 block 尺寸从源码 aclnn_trans_matmul_weight.cpp 中的常量定义可以确认MIN_SIZE_PER_BLOCK_FLOAT16 16、MIN_SIZE_PER_BLOCK_INT8 32。源码中的对齐实现接口内部统一调用CalculateMatmulWeightSize()完成计算核心逻辑见 aclnn_trans_matmul_weight.cpp 中CalculateMatmulWeightSize函数可以归纳为三点仅对最后两根轴做对齐注释明确写着多维场景只有最后两根轴需要做对齐处理。对于 3 维及以上的 batch 权重如[batch, n, k]batch等前导维度按原值累乘只有倒数第二维n与最后一维k参与对齐Float16/BFloat16 两维均对齐到 16INT8 时倒数第二维对齐到 16、最后一维对齐到 32。这一实现与公式完全对应文档公式是二维情形Shapesize[0]、Shapesize[1]的展开而源码支持 26 维的通用 batch 权重场景。函数原型aclnnStatus aclnnCalculateMatmulWeightSizeV2( const aclIntArray *tensorShape, aclDataType dataType, uint64_t *weightTensorSize)接口声明位于 aclnn_trans_matmul_weight.h头文件中的注释说明该接口用于计算调用aclnnMatMul、aclnnMm、aclnnWeightQuantBatchMatmulV2、aclnnWeightQuantBatchMatmulV3、aclnnQuantMatmulV3等接口时传入的 weight tensor 需要占用的元素大小。参数说明参数名输入/输出描述数据类型维度(shape)使用说明tensorShapeaclIntArray *输入用于表达该次 Matmul 载入权重矩阵的 Shape即公式中的 ShapesizeINT642-6支持 2-6 维即batchnk其中 batch 表示权重矩阵的批次大小支持 0-4 维n 表示单个 batch 权重矩阵第 1 维的大小k 表示单个 batch 权重矩阵第 2 维的大小。不支持空 ArraydataTypeaclDataType输入weight 的 DtypeFLOAT16、BFLOAT16、INT8--weightTensorSizeuint64_t *输出转换为 NZ 格式所占用的空间大小单位为元素个数即公式中的 result---对应源码中的校验逻辑维度校验由CheckShapeDimV2()完成要求维度数在MIN_INT8_DIM_NUM_ND(2) 到MAX_INT8_DIM_NUM_ND(6) 之间否则返回ACLNN_ERR_PARAM_INVALID空 shape 校验由CheckNonZeroShape()完成shape 中任意一维为 0 时同样返回ACLNN_ERR_PARAM_INVALIDNo zeros are allow in shape。返回值说明返回值为aclnnStatus状态码具体码值含义参见 aclnn 返回码说明。第一段接口完成入参校验出现以下场景时报错返回值错误码描述ACLNN_ERR_PARAM_NULLPTR161001输入是空指针tensorShape 或 weightTensorSize 为 nullptrACLNN_ERR_PARAM_INVALID161002不支持输入空 Tensorshape 中存在 0 维输入 shape 的维度不满足要求输入的数据类型不满足要求ACLNN_ERR_RUNTIME_ERROR361001产品型号不支持上述错误路径在单元测试 test_aclnn_trans_matmul_weight_l2.cpp 中均有覆盖例如aclnnCalculateMatmulWeightSizeV2(nullptr, dataType, weightSize)→ 期望ACLNN_ERR_PARAM_NULLPTRaclnnCalculateMatmulWeightSizeV2(tensorShape, dataType, nullptr)→ 期望ACLNN_ERR_PARAM_NULLPTRshape 含 0 维{0, 16}→ 期望ACLNN_ERR_PARAM_INVALIDshape 为 7 维 → 期望ACLNN_ERR_PARAM_INVALID。约束说明确定性计算aclnnCalculateMatmulWeightSizeV2默认确定性实现该接口本身为纯 host 端计算无设备侧不确定性。调用示例示例代码如下仅供参考具体编译和执行过程请参考 编译与运行样例。示例展示了完整的权重预处理链路用aclnnCalculateMatmulWeightSizeV2计算容量 → 申请对齐后的 device 内存 → 调用aclnnTransMatmulWeight将 ND 权重转为 NZ 格式 → 配合aclnnWeightQuantBatchMatmulV2执行量化 Matmul。#include iostream #include vector #include acl/acl.h #include aclnnop/aclnn_cast.h #include aclnnop/aclnn_weight_quant_batch_matmul_v2.h #include aclnnop/aclnn_trans_matmul_weight.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::vectorint64_t shape) { int64_t shapeSize 1; for (auto i : shape) { shapeSize * i; } return shapeSize; } 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 CreateAclTensorWeight(const std::vectorT hostData, const std::vectorint64_t shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size static_castuint64_t(GetShapeSize(shape)); const aclIntArray* mat2Size aclCreateIntArray(shape.data(), shape.size()); auto ret aclnnCalculateMatmulWeightSizeV2(mat2Size, dataType, size); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCalculateMatmulWeightSizeV2 failed. ERROR: %d\n, ret); return ret); size * sizeof(T); // 调用aclrtMalloc申请device侧内存 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::vectorint64_t strides(shape.size(), 1); for (int64_t i shape.size() - 2; i 0; i--) { strides[i] shape[i 1] * strides[i 1]; } std::vectorint64_t storageShape; storageShape.push_back(GetShapeSize(shape)); // 调用aclCreateTensor接口创建aclTensor *tensor aclCreateTensor(shape.data(), shape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, storageShape.data(), storageShape.size(), *deviceAddr); return 0; } template typename T int CreateAclTensor(const std::vectorT hostData, const std::vectorint64_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::vectorint64_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 ACL_SUCCESS, LOG_PRINT(Init acl failed. ERROR: %d\n, ret); return ret); // 2.构造输入与输出需要根据API的接口自定义构造 std::vectorint64_t xShape {16, 32}; std::vectorint64_t weightShape {32, 16}; std::vectorint64_t yShape {16, 16}; void* xDeviceAddr nullptr; void* weightDeviceAddr nullptr; void* yDeviceAddr nullptr; aclTensor* x nullptr; aclTensor* weight nullptr; aclTensor* y nullptr; std::vectorfloat xHostData(512, 1); std::vectorint8_t weightHostData(512, 1); std::vectorfloat yHostData(256, 0); std::vectorint64_t antiquantScaleShape {16}; void* antiquantScaleDeviceAddr nullptr; aclTensor* antiquantScale nullptr; std::vectorfloat antiquantScaleHostData(16, 1); // 创建x aclTensor ret CreateAclTensor(xHostData, xShape, xDeviceAddr, aclDataType::ACL_FLOAT, x); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建weight aclTensor ret CreateAclTensorWeight(weightHostData, weightShape, weightDeviceAddr, aclDataType::ACL_INT8, weight); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建y aclTensor ret CreateAclTensor(yHostData, yShape, yDeviceAddr, aclDataType::ACL_FLOAT, y); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建antiquantScale aclTensor ret CreateAclTensor(antiquantScaleHostData, antiquantScaleShape, antiquantScaleDeviceAddr, aclDataType::ACL_FLOAT, antiquantScale); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建xFp16 aclTensor void* xFp16DeviceAddr nullptr; aclTensor* xFp16 nullptr; ret CreateAclTensor(xHostData, xShape, xFp16DeviceAddr, aclDataType::ACL_FLOAT16, xFp16); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建antiquantScale aclTensor void* antiquantScaleFp16DeviceAddr nullptr; aclTensor* antiquantScaleFp16 nullptr; ret CreateAclTensor(antiquantScaleHostData, antiquantScaleShape, antiquantScaleFp16DeviceAddr, aclDataType::ACL_FLOAT16, antiquantScaleFp16); CHECK_RET(ret ACL_SUCCESS, return ret); // 创建yFp16 aclTensor void* yFp16DeviceAddr nullptr; aclTensor* yFp16 nullptr; ret CreateAclTensor(yHostData, yShape, yFp16DeviceAddr, aclDataType::ACL_FLOAT16, yFp16); CHECK_RET(ret ACL_SUCCESS, return ret); // 3.调用CANN算子库API需要修改为具体的API名称 uint64_t workspaceSize 0; aclOpExecutor* executor; void* workspaceAddr nullptr; // 调用TransWeight ret aclnnTransMatmulWeightGetWorkspaceSize(weight, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnTransMatmulWeightGetWorkspaceSize failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 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); } // 调用aclnnTransMatmulWeight第二段接口 ret aclnnTransMatmulWeight(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnTransMatmulWeight failed. ERROR: %d\n, ret); return ret); workspaceSize 0; // 调用cast生成FP16的输入 ret aclnnCastGetWorkspaceSize(x, aclDataType::ACL_FLOAT16, xFp16, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCastGetWorkspaceSize0 failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 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); } ret aclnnCast(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCast0 failed. ERROR: %d\n, ret); return ret); ret aclrtSynchronizeStream(stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtSynchronizeStream failed. ERROR: %d\n, ret); return ret); ret aclnnCastGetWorkspaceSize(antiquantScale, aclDataType::ACL_FLOAT16, antiquantScaleFp16, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCastGetWorkspaceSize1 failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 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); } ret aclnnCast(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCast1 failed. ERROR: %d\n, ret); return ret); ret aclrtSynchronizeStream(stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtSynchronizeStream failed. ERROR: %d\n, ret); return ret); // 调用aclnnWeightQuantBatchMatmulV2第一段接口 ret aclnnWeightQuantBatchMatmulV2GetWorkspaceSize(xFp16, weight, antiquantScaleFp16, nullptr, nullptr, nullptr, nullptr, 0, yFp16, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnWeightQuantBatchMatmulV2GetWorkspaceSize failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 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); } // 调用aclnnWeightQuantBatchMatmulV2第二段接口 ret aclnnWeightQuantBatchMatmulV2(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnWeightQuantBatchMatmulV2 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); // 将输出转为FP32 ret aclnnCastGetWorkspaceSize(yFp16, aclDataType::ACL_FLOAT, y, workspaceSize, executor); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCastGetWorkspaceSize2 failed. ERROR: %d\n, ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 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); } ret aclnnCast(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclnnCast2 failed. ERROR: %d\n, ret); return ret); ret aclrtSynchronizeStream(stream); CHECK_RET(ret ACL_SUCCESS, LOG_PRINT(aclrtSynchronizeStream failed. ERROR: %d\n, ret); return ret); // 5.获取输出的值将device侧内存上的结果拷贝至host侧需要根据具体API的接口定义修改 auto size GetShapeSize(yShape); std::vectorfloat resultData(size, 0); ret aclrtMemcpy(resultData.data(), resultData.size() * sizeof(resultData[0]), yDeviceAddr, size * sizeof(resultData[0]), 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]); } // 6.释放aclTensor和aclScalar需要根据具体API的接口定义修改 aclDestroyTensor(x); aclDestroyTensor(weight); aclDestroyTensor(antiquantScale); aclDestroyTensor(y); aclDestroyTensor(xFp16); aclDestroyTensor(antiquantScaleFp16); aclDestroyTensor(yFp16); // 7.释放device资源 aclrtFree(xDeviceAddr); aclrtFree(weightDeviceAddr); aclrtFree(antiquantScaleDeviceAddr); aclrtFree(yDeviceAddr); aclrtFree(xFp16DeviceAddr); aclrtFree(antiquantScaleFp16DeviceAddr); aclrtFree(yFp16DeviceAddr); if (workspaceSize 0) { aclrtFree(workspaceAddr); } aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); return 0; }示例中的关键要点容量即 storage 容量CreateAclTensorWeight中先按逻辑 shape 初始化size再调用aclnnCalculateMatmulWeightSizeV2将其刷新为对齐后的容量元素个数随后乘以sizeof(T)得到字节数用于aclrtMalloc创建aclTensor时把该容量写入 storageShape逻辑 shape 保持不变。这与 官方 example 的做法一致且 example 中特别提醒只拷贝逻辑大小的数据到 device对齐产生的 padding 字节内容未定义因此后续按元素校验时应只校验逻辑范围内的数据两段式接口调用模式aclnnTransMatmulWeightGetWorkspaceSize/aclnnTransMatmulWeight是标准的 aclnn 两段式接口与aclnnCalculateMatmulWeightSizeV2搭配形成计算容量 → 转换格式 → 执行 Matmul的完整链路两段式接口的通用说明可参考 两段式接口说明配套校验示例中 weight 为 INT8 类型weightShape {32, 16}调用aclnnCalculateMatmulWeightSizeV2时dataType传ACL_INT8两维分别按 16 和 32 对齐后容量为Align(32,16) * Align(16,32) 32 * 32 1024大于逻辑元素数 512符合容量不小于逻辑大小的预期。与 aclnnTransMatmulWeight 的协作机制从源码结构看aclnn_trans_matmul_weight.cpp 中aclnnTransMatmulWeightGetWorkspaceSize的完整处理流程为参数检查非空、dtype 在支持列表内如 DAV_2201 架构支持 Float16/BFloat16/Int8/Float8 等按GetDtypeSupportList()区分架构、存储格式必须为 ND、维度不超过上限对 INT8 输入按CheckShapeDimV2校验 2-6 维其他 dtype 按CheckShapeDim校验非 910B 架构要求严格 2 维再次调用CalculateMatmulWeightSize计算对齐后容量通过l0op::Contiguous确保权重连续再用l0op::TransData将其转换为FORMAT_FRACTAL_NZ最后通过l0op::TensorMove把结果写回原 tensor并刷新其 originalShape、storageShape 与 storageFormat。也就是说aclnnCalculateMatmulWeightSizeV2计算出的容量正是后续aclnnTransMatmulWeight原地写入 NZ 格式数据所需的存储下限若用户自行分配权重内存而容量不足转换阶段将因越界而失败。相关接口与延伸阅读同一头文件 aclnn_trans_matmul_weight.h 中还提供了旧版接口aclnnCalculateMatmulWeightSize仅两参数、默认按 Float16 对齐规则计算且维度校验更严格新场景建议使用本文介绍的 V2 版本以获得 INT8 支持与多维 batch 能力。相关文档还包括aclnnCalculateMatmulWeightSize.md旧版容量计算接口aclnnTransMatmulWeight.mdND → NZ 权重格式转换接口trans_data READMETransData 算子的格式转换矩阵其中 ND FRACTAL_NZ 对 float16/bfloat16 的 C0 为 16、对 int8/uint8 的 C0 为 32与本接口的对齐粒度一致test_aclnn_calculate_matmul_weight_size_v2.cpparch35 架构下的可运行 example演示计算容量 → 分配内存 → 转换权重 → 回读校验的完整流程。【免费下载链接】ops-math本项目是CANN提供的数学类基础计算算子库实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-math创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考