- 人工智能
- 算子库
- 深度学习
- CANN
- Ascend
【免费下载链接】ops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
本文以 CANN ops-nn 仓库中的 aclnnSparse4to2QuantMatmulWeightNz 算子文档 为骨架,结合仓库内 op_api、op_host、op_kernel 等源码实现,系统讲解稀疏 4:2(4 选 2)量化矩阵乘算子的功能语义、两段式接口用法、参数约束、配套压缩预处理接口以及完整可运行的调用示例。读完本文,你将能够在 Atlas A2/A3 训练与推理系列产品上,独立完成从权重稀疏化压缩到aclnnSparse4to2QuantMatmulWeightNz双段接口调用的全流程开发。
功能说明与计算公式
aclnnSparse4to2QuantMatmulWeightNz是 CANN ops-nn 中Sparse4to2QuantMatmul算子的 aclnn 接口封装,核心功能是完成稀疏 4:2 量化的矩阵乘计算。所谓"稀疏 4:2",指的是权重矩阵按每 4 个元素一组进行结构化稀疏,每组中至少保留 2 个零元素,压缩后每组仅保留 2 个非零元素及其位置索引,从而在不损失精度的前提下显著降低权重存储与计算量。
算子计算公式如下:
$$ out = x@sparseWeight \times sparseWeightScale \times xScale + bias $$
其中:
x为 INT8 激活矩阵(左矩阵),shape 为(m, k);sparseWeight为经过压缩处理的稀疏右矩阵,shape 为(n, k_half);sparseWeightScale、xScale为 FLOAT32 量化缩放因子,分别按 n、m 维度逐行缩放;bias为可选的 BFLOAT16 偏置,shape 为(n,);out为 BFLOAT16 输出,shape 为(m, n)。
从公式可以看出,该算子将"反量化 + 矩阵乘 + 量化"融合为一次 NPU 计算:先在 INT8 域完成稀疏矩阵乘x@sparseWeight,再与两个缩放因子相乘,最后累加偏置并输出 BFLOAT16 结果。
产品支持情况
根据算子文档与 sparse4to2quant_matmul_def.cpp 中的 AICore 配置注册(ascend910b、ascend910_93),本算子支持以下产品形态:
| 产品 | 是否支持 |
|---|---|
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
源码中 aclnn_sparse4to2quant_matmul_weight_nz.cpp 的第一段接口入口同样做了平台校验,仅允许ASCEND910_93与ASCEND910B两个 SoC 版本执行,其他平台直接返回ACLNN_ERR_PARAM_INVALID。
前置步骤:权重压缩接口 aclnnTransSparse4to2Para
aclnnSparse4to2QuantMatmulWeightNz的输入sparseWeight与index并非原始权重,而是必须经过配套的压缩预处理接口 aclnnTransSparse4to2Para 处理后得到的压缩矩阵与索引矩阵。该接口的完整文档位于 aclnnTransSparse4to2Para.md。
其功能为:对结构化稀疏的权重矩阵(每 4 个元素中至少有 2 个零)进行压缩预处理,输出压缩后的稀疏矩阵以及对应的索引矩阵。压缩过程中,原始矩阵中的每 4 个元素会在 index 矩阵中生成 2 个 2 位索引,并按照固定规则编码。
函数原型如下:
aclnnStatus aclnnTransSparse4to2Para( const int8_t* weight, aclIntArray* shape, int8_t** sparseWeight, int64_t** sparseWeightDims, uint64_t* sparseWeightDimsNum, uint8_t** index, int64_t** indexDims, uint64_t* indexDimsNum)参数要点(详见原文档参数表):
| 参数 | 输入/输出 | 说明 |
|---|---|---|
weight | 输入 | 未压缩的稀疏右矩阵,shape(n, k),INT8,ND 格式,须满足每 4 个元素至少 2 个零 |
shape | 输入 | 权重 shape,通过aclCreateIntArray接口创建 |
sparseWeight | 输出 | 压缩后的右矩阵,内存由调用者释放 |
sparseWeightDims | 输出 | 压缩后右矩阵 StorageShape 数组指针首地址,shape 为(ceil(k_half/32), ceil(n/16), 16, 32),其中k_half = ceil(k/8) * 8 / 2 |
sparseWeightDimsNum | 输出 | StorageShape 数组维度 |
index | 输出 | 压缩后的索引矩阵,内存由调用者释放 |
indexDims | 输出 | 索引矩阵 StorageShape 数组指针首地址,shape 为(ceil(k_half/32), ceil(n/16), 16, 8) |
indexDimsNum | 输出 | 索引矩阵 StorageShape 数组维度 |
该接口校验失败时返回ACLNN_ERR_PARAM_NULLPTR(161001,weight 为空指针)或ACLNN_ERR_PARAM_INVALID(161002,shape/format/dtype 不满足要求,或数据不满足每 4 个元素至少 2 个零)。
两段式接口与函数原型
与其他 aclnn 算子一致,aclnnSparse4to2QuantMatmulWeightNz采用两段式接口设计(详见 docs/zh/context/two_phase_api.md):必须先调用第一段接口aclnnSparse4to2QuantMatmulWeightNzGetWorkspaceSize完成入参校验、构图并计算出所需 workspace 大小,再调用第二段接口aclnnSparse4to2QuantMatmulWeightNz在指定 stream 上执行计算。
第一段接口原型:
aclnnStatus aclnnSparse4to2QuantMatmulWeightNzGetWorkspaceSize( const aclTensor *x, const aclTensor *sparseWeight, const aclTensor *index, const aclTensor *xScale, const aclTensor *sparseWeightScale, const aclTensor *biasOptional, aclTensor *out, uint64_t *workspaceSize, aclOpExecutor **executor)第二段接口原型:
aclnnStatus aclnnSparse4to2QuantMatmulWeightNz( void *workspace, uint64_t workspaceSize, aclOpExecutor *executor, aclrtStream stream)两个接口的声明位于 aclnn_sparse4to2quant_matmul_weight_nz.h,头文件为aclnnop/aclnn_sparse4to2quant_matmul_weight_nz.h。
第一段接口:GetWorkspaceSize 参数详解
以下参数表完整继承自算子文档,并补充了源码校验逻辑作为佐证:
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
x | 输入 | 矩阵乘运算中的左矩阵 | 仅支持 2 维输入,shape 可表达为(m, k) | INT8 | ND | 2 | × |
sparseWeight | 输入 | 矩阵乘运算中的右矩阵,经过aclnnTransSparse4to2Para接口处理后的稀疏矩阵 | 仅支持 2 维输入,ViewShape 可表达为(n, k_half),其中k_half = ceil(k/8) * 8 / 2;StorageShape 从aclnnTransSparse4to2Para出参sparseWeightDims和sparseWeightDimsNum获取,为 4 维,与 ViewShape 的关系为(ceil(k_half/32), ceil(n/16), 16, 32) | INT8 | FRACTAL_NZ | 2 | × |
index | 输入 | 经过aclnnTransSparse4to2Para接口压缩后计算得到的索引矩阵 | 4 维输入,ViewShape 可表达为(ceil(k_half/32), ceil(n/16), 16, 8);StorageShape 从aclnnTransSparse4to2Para出参indexDims和indexDimsNum获取,为 4 维,与 ViewShape 相同 | UINT8 | ND | 4 | × |
xScale | 输入 | 量化参数,缩放因子 | shape 可表达为(m,) | FLOAT32 | ND | 1 | × |
sparseWeightScale | 输入 | 量化参数,缩放因子 | shape 可表达为(n,) | FLOAT32 | ND | 1 | × |
biasOptional | 输入 | 可选偏置 | shape 可表达为(n,) | BFLOAT16 | ND | 1 | × |
out | 输出 | 输出的张量,公式中的 out | shape 可表达为(m, n) | BFLOAT16 | ND | 2 | × |
workspaceSize | 输出 | 返回需要在 Device 侧申请的 workspace 大小 | - | - | - | - | - |
executor | 输出 | 返回 op 执行器,包含了算子计算流程 | - | - | - | - | - |
关于 ViewShape 与 StorageShape:sparseWeight在逻辑视图(ViewShape)上是 2 维的(n, k_half),但在物理存储(StorageShape)上是 4 维的 FRACTAL_NZ 分形格式(ceil(k_half/32), ceil(n/16), 16, 32);index的 ViewShape 与 StorageShape 均为 4 维。创建这类张量时需要使用aclCreateTensor同时传入 view shape 与 storage shape(见后文示例中的CreateSparseTensor辅助函数)。源码中 GetWeightNzShape 会按上述规则计算期望的 NZ 存储 shape,并与传入的sparseWeightStorageShape 严格比对,不一致时返回ACLNN_ERR_PARAM_INVALID。
返回值与错误码
两段接口均返回aclnnStatus状态码,具体参见 docs/zh/context/aclnn_return_code.md。第一段接口完成入参校验,出现以下场景时报错:
| 返回码 | 错误码 | 描述 |
|---|---|---|
ACLNN_ERR_PARAM_NULLPTR | 161001 | 传入的必选输入 x、sparseWeight、index 或 out 是空指针 |
ACLNN_ERR_PARAM_INVALID | 161002 | 输入输出的数据类型、shape、format、dtype 不在支持的范围之内 |
源码中 CheckParams 完整执行了四步校验,与文档错误码一一对应:
- CheckDtypeValid:x、sparseWeight 必须为 INT8,index 必须为 UINT8,bias 必须为 BF16,xScale、sparseWeightScale 必须为 FLOAT32,out 必须为 BF16;
- CheckFormatValid:x/index/xScale/sparseWeightScale/bias/out 必须为 ND,sparseWeight 主格式必须为 FRACTAL_NZ;
- CheckShape:k 维需满足 x 的 k 维 8 对齐后等于 sparseWeight k 维的两倍(
CeilAlign(xK, 8) == 2 * weightK),bias 与 sparseWeightScale 的第 0 维等于 n,xScale 第 0 维等于 m,out shape 必须为(m, n);同时校验 k 不超过 65535(LAST_AXIS_LIMIT); - CheckEmptyTensor:不支持空 tensor。
第二段接口:执行计算参数详解
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
workspace | 输入 | 在 Device 侧申请的 workspace 内存地址 |
workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口aclnnSparse4to2QuantMatmulWeightNzGetWorkspaceSize获取 |
executor | 输入 | op 执行器,包含了算子计算流程 |
stream | 输入 | 指定执行任务的 Stream |
第二段接口的源码实现非常简洁,直接调用通用执行器完成计算:
aclnnStatus aclnnSparse4to2QuantMatmulWeightNz(void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, aclrtStream stream) { L2_DFX_PHASE_2(aclnnSparse4to2QuantMatmulWeightNz); return CommonOpExecutorRun(workspace, workspaceSize, executor, stream); }约束说明
- 确定性说明:Atlas A3 训练系列/推理系列产品、Atlas A2 训练系列/推理系列产品上,
aclnnSparse4to2QuantMatmulWeightNz为默认确定性实现(即相同输入产生确定输出)。 - k 维上限:x 的最后一维即 shape 描述中 k 的值不能超过 65535。该限制在源码 MaxDimCheck 中通过
LAST_AXIS_LIMIT常量强制校验。 - 缩放因子必选:当前只支持
sparseWeightScale、xScale均不为 nullptr 的场景。这也是 tiling 阶段 AnalyzeDtype 中的硬性要求——二者为空时直接报错。 - 不支持空 tensor、不支持非连续 tensor(参见 README.md 约束说明)。
完整调用示例
示例代码取自算子文档,仓库内另有可直接参考的工程样例 test_aclnn_sparse4to2quant_matmul.cpp(代码与文档示例基本一致,均为完整可编译的 280 行程序)。具体编译与运行方法请参考 docs/zh/context/compile_and_run_sample.md。
#include <iostream> #include <memory> #include <vector> #include <stdlib.h> #include "acl/acl.h" #include "aclnnop/aclnn_sparse4to2quant_matmul_weight_nz.h" #define CHECK_RET(cond, return_expr) \ do { \ if (!(cond)) { \ return_expr; \ } \ } while (0) #define CHECK_FREE_RET(cond, return_expr) \ do { \ if (!(cond)) { \ Finalize(deviceId, stream); \ return_expr; \ } \ } while (0) #define LOG_PRINT(message, ...) \ do { \ printf(message, ##__VA_ARGS__); \ } while (0) #define CREATE_TENSOR(hostData, shape, deviceAddr, dtype, tensor) \ ret = CreateAclTensor(hostData, shape, &deviceAddr, dtype, &tensor); \ std::unique_ptr<aclTensor, aclnnStatus (*)(const aclTensor*)> tensor##Ptr(tensor, aclDestroyTensor); \ std::unique_ptr<void, aclError (*)(void*)> deviceAddr##Ptr(deviceAddr, aclrtFree); \ CHECK_RET(ret == ACL_SUCCESS, return ret) #define CREATE_SPARSE_TENSOR(hostData, weightShape, storageShape, deviceAddr, dataType, tensor) \ ret = CreateSparseTensor(hostData, weightShape, storageShape, &deviceAddr, dataType, &tensor); \ std::unique_ptr<aclTensor, aclnnStatus (*)(const aclTensor*)> tensor##Ptr(tensor, aclDestroyTensor); \ std::unique_ptr<void, aclError (*)(void*)> deviceAddr##Ptr(deviceAddr, aclrtFree); \ CHECK_RET(ret == ACL_SUCCESS, return ret) int64_t GetShapeSize(const std::vector<int64_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 CreateSparseTensor( const T* sparseWeightData, const std::vector<int64_t>& viewShape, const std::vector<int64_t>& storageShape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { auto size = static_cast<uint64_t>(GetShapeSize(storageShape)) * 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, sparseWeightData, size, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclrtMemcpy failed. ERROR: %d\n", ret); return ret); // 计算连续tensor的strides std::vector<int64_t> strides(viewShape.size(), 1); for (int64_t i = viewShape.size() - 2; i >= 0; i--) { strides[i] = viewShape[i + 1] * strides[i + 1]; } // 调用aclCreateTensor接口创建aclTensor(同时传入view shape与storage shape) *tensor = aclCreateTensor( viewShape.data(), viewShape.size(), dataType, strides.data(), 0, aclFormat::ACL_FORMAT_ND, storageShape.data(), storageShape.size(), *deviceAddr); return 0; } template <typename T> int CreateAclTensor( const std::vector<T>& hostData, const std::vector<int64_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侧内存上 if (hostData.size() > 0) { 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::vector<int64_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; } void Finalize(int32_t deviceId, aclrtStream stream) { aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); } void GenRandomMask(std::vector<size_t>& masks) { masks[0] = random() % 4; masks[1] = random() % 4; while (masks[1] == masks[0]) { masks[1] = random() % 4; } } void GenRandomSparseData(std::vector<int8_t>& weightHostData) { srandom(233U); std::vector<size_t> masks(2, 0UL); constexpr size_t step = 4UL; for (size_t i = 0; i < weightHostData.size(); i += step) { GenRandomMask(masks); for (auto mask : masks) { weightHostData[i + mask] = 0; } } } std::vector<int64_t> GenStorageShape(int64_t* dims, uint64_t dimsNum) { std::vector<int64_t> storageShape; for (uint64_t i = 0UL; i < dimsNum; i++) { storageShape.push_back(dims[i]); } return storageShape; } int aclnnSparse4to2QuantMatmulWeightNzTest(int32_t deviceId, 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的接口自定义构造 int64_t m = 64L; int64_t k = 512L; int64_t n = 128L; std::vector<int64_t> xShape = {m, k}; std::vector<int64_t> weightShape = {n, k}; std::vector<int64_t> indexShape = {n, (k + 7) / 8}; std::vector<int64_t> biasShape = {n}; std::vector<int64_t> xScaleShape = {m}; std::vector<int64_t> weightScaleShape = {n}; std::vector<int64_t> outShape = {m, n}; void* xDeviceAddr = nullptr; void* sparseWeightDeviceAddr = nullptr; void* indexDeviceAddr = nullptr; void* biasDeviceAddr = nullptr; void* xScaleDeviceAddr = nullptr; void* weightScaleDeviceAddr = nullptr; void* outDeviceAddr = nullptr; aclTensor* x = nullptr; aclTensor* sparseWeight = nullptr; aclTensor* index = nullptr; aclTensor* bias = nullptr; aclTensor* xScale = nullptr; aclTensor* weightScale = nullptr; aclTensor* out = nullptr; std::vector<int8_t> xHostData(GetShapeSize(xShape), 1); std::vector<int8_t> weightHostData(GetShapeSize(weightShape), 1); std::vector<uint16_t> biasHostData(GetShapeSize(biasShape), 1); // 实际上是bfloat16半精度方式 std::vector<float> xScaleHostData(GetShapeSize(xScaleShape), 1); std::vector<float> weightScaleHostData(GetShapeSize(weightScaleShape), 1); GenRandomSparseData(weightHostData); int8_t* sparseWeightHostData = nullptr; uint8_t* indexHostData = nullptr; int64_t* sparseWeightDims = nullptr; uint64_t sparseWeightDimsNum = 0UL; int64_t* indexDims = nullptr; uint64_t indexDimsNum = 0UL; aclIntArray* weightShapeArray = aclCreateIntArray(weightShape.data(), weightShape.size()); ret = aclnnTransSparse4to2Para( weightHostData.data(), weightShapeArray, &sparseWeightHostData, &sparseWeightDims, &sparseWeightDimsNum, &indexHostData, &indexDims, &indexDimsNum); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnTransSparse4to2Para failed. ERROR: %d\n", ret); return ret); std::unique_ptr<int8_t[]> sparseWeightHostDataPtr(sparseWeightHostData); std::unique_ptr<uint8_t[]> indexHostDataPtr(indexHostData); std::unique_ptr<int64_t[]> sparseWeightDimsPtr(sparseWeightDims); std::unique_ptr<int64_t[]> indexDimsPtr(indexDims); CREATE_TENSOR(xHostData, xShape, xDeviceAddr, aclDataType::ACL_INT8, x); weightShape.back() = (weightShape.back() + 7) / 8 * 8 / 2; // 4选2后K轴向上8对齐后减半 auto sparseWeightStorageShape = GenStorageShape(sparseWeightDims, sparseWeightDimsNum); CREATE_SPARSE_TENSOR( sparseWeightHostData, weightShape, sparseWeightStorageShape, sparseWeightDeviceAddr, aclDataType::ACL_INT8, sparseWeight); auto indexStorageShape = GenStorageShape(indexDims, indexDimsNum); CREATE_SPARSE_TENSOR(indexHostData, indexShape, indexStorageShape, indexDeviceAddr, aclDataType::ACL_UINT8, index); CREATE_TENSOR(biasHostData, biasShape, biasDeviceAddr, aclDataType::ACL_BF16, bias); CREATE_TENSOR(xScaleHostData, xScaleShape, xScaleDeviceAddr, aclDataType::ACL_FLOAT, xScale); CREATE_TENSOR(weightScaleHostData, weightScaleShape, weightScaleDeviceAddr, aclDataType::ACL_FLOAT, weightScale); CREATE_TENSOR(std::vector<uint16_t>(), outShape, outDeviceAddr, aclDataType::ACL_BF16, out); uint64_t workspaceSize = 0; aclOpExecutor* executor; void* workspaceAddr = nullptr; // 调用aclnnSparse4to2QuantMatmulWeightNz第一段接口 ret = aclnnSparse4to2QuantMatmulWeightNzGetWorkspaceSize( x, sparseWeight, index, xScale, weightScale, bias, out, &workspaceSize, &executor); CHECK_RET( ret == ACL_SUCCESS, LOG_PRINT("aclnnSparse4to2QuantMatmulWeightNzGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); // 根据第一段接口计算出的workspaceSize申请device内存 std::unique_ptr<void, aclError (*)(void*)> workspaceAddrPtrTrans(nullptr, aclrtFree); 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); workspaceAddrPtrTrans.reset(workspaceAddr); } // 调用aclnnSparse4to2QuantMatmulWeightNz第二段接口 ret = aclnnSparse4to2QuantMatmulWeightNz(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnSparse4to2QuantMatmulWeightNz 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); // 5. 获取输出的值,将device侧内存上的结果拷贝至host侧,需要根据具体API的接口定义修改 auto size = GetShapeSize(outShape); // C语言中无法直接打印bf16的数据,需要用uint16读出来,自行通过二进制转成bf16 std::vector<uint16_t> resultData(size, 0); ret = aclrtMemcpy( resultData.data(), resultData.size() * sizeof(resultData[0]), outDeviceAddr, 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: %u\n", i, resultData[i]); } return ACL_SUCCESS; } int main() { // 1.(固定写法)device/stream初始化,参考acl API手册 // 根据自己的实际device填写deviceId int32_t deviceId = 0; aclrtStream stream; auto ret = aclnnSparse4to2QuantMatmulWeightNzTest(deviceId, stream); CHECK_FREE_RET( ret == ACL_SUCCESS, LOG_PRINT("aclnnSparse4to2QuantMatmulWeightNzTest failed. ERROR: %d\n", ret); return ret); Finalize(deviceId, stream); return 0; }示例关键步骤解读
- 资源初始化:
aclInit→aclrtSetDevice→aclrtCreateStream,固定写法; - 构造输入输出:示例取
m=64, k=512, n=128。权重初始化为全 1 后,通过GenRandomSparseData在每个 4 元素组中随机置零 2 个元素,构造出满足"每 4 个元素至少 2 个零"的结构化稀疏数据; - 调用
aclnnTransSparse4to2Para压缩权重:输入原始weightHostData与 shape 数组,输出sparseWeightHostData、indexHostData及其各自 dims。注意压缩接口的输出内存由调用者释放,示例用std::unique_ptr托管; - 创建稀疏张量:
weightShape.back()修改为(k + 7) / 8 * 8 / 2(即k_half,4 选 2 后 K 轴向上 8 对齐再减半),并配合压缩接口返回的 StorageShape,通过CreateSparseTensor创建带 view/storage 双 shape 的aclTensor; - 两段式调用:先
GetWorkspaceSize获取workspaceSize与executor,再按需aclrtMalloc申请 workspace,最后执行第二段接口; - 结果回读:
aclrtSynchronizeStream同步后,将 out 从 Device 拷贝至 Host。由于 BF16 无法直接打印,示例用uint16_t读出原始二进制,自行按需转换。
源码级实现原理
算子定义与形状推导
Sparse4to2QuantMatmul 的 OpDef 声明了 6 个输入与 1 个输出:
- 必选输入:
x(INT8/ND)、sparse_weight(INT8/FRACTAL_NZ)、index(UINT8/ND); - 可选输入:
x_scale(FLOAT32/ND)、sparse_weight_scale(FLOAT32/ND)、bias(BF16/ND); - 输出
y(BF16/ND),属性dtype; - AICore 配置仅注册
ascend910b与ascend910_93两个平台,并开启动态 shape、动态 rank、动态 format 支持。
形状推导 sparse4to2quant_matmul_infershape.cpp 的逻辑与 aclnn 层校验一致:x 与 sparseWeight 均须为 2 维,x 的 k 维 8 对齐后必须等于 sparseWeight k 维的两倍,输出 shape 为(m, n)。
Tiling 策略
sparse4to2quant_matmul_tiling.cpp 负责将 M/N/K 三维切分为 NPU 多核可执行的基本块:
- 数据类型解析(
AnalyzeDtype)确认 x/sparseWeight 为 INT8、scale 为 FLOAT、bias 为 BF16、输出为 BF16; - 输入解析(
AnalyzeInputs)从原始 shape 提取mSize/kaSize/kbSize/nSize,并校验 x 的 k(8 对齐后)为 sparseWeight k 的两倍; - 基本块切分(
CalcL0Tiling/CalcL1Tiling):baseM/baseN/baseK 候选集、L0A/L0B/L0C 容量约束、L1 depthA1/depthB1 流水深度与 index 空间联合裁剪; - L2 缓存切分(
DoL2CacheTiling)与多核计算顺序(ROW_FIRST/COL_FIRST)决策; - workspace 计算(
GetWorkspaceSize):16MB系统 workspace 加上baseM * baseN * usedCoreNum * 2 * sizeof(int32)的用量计算。
值得注意的是 tiling 中针对稀疏特性做了专门优化:稀疏权重访问数据量系数SPARSE_WEIGHT_RATIO = 0.625(即 5/8),以及 index 空间按depthBSize / 4估算,体现了"压缩后少读一半权重"的稀疏加速语义。
Kernel 入口
sparse4to2quant_matmul.cpp 中 kernel 入口sparse4to2quant_matmul接收x / sparseWeight / index / xScale / sparseWeightScale / bias / y / workSpace / tiling九个 GM 地址参数,注册 tiling 数据后实例化Sparse4to2QuantMatmul<int8_t, int8_t, DTYPE_Y, format_x1, format_x2>并执行Init+Process。从类型模板可以看出:A 矩阵(x)以 ND/INT8 进入,B 矩阵(sparseWeight)以 FRACTAL_NZ/INT8 进入,与 aclnn 层与算子定义中的格式约束完全一致。
二进制配置
算子二进制映射配置见 sparse4to2quant_matmul_binary.json(ascend910_93目录下存在同构配置),声明了Sparse4to2QuantMatmul_ND_NZ_int8_int8_fp32_fp16二进制文件对应的输入输出 dtype/format 组合,以及属性dtype的默认值 27(对应 BF16 输出数据类型枚举)。
典型使用链路总结
完整使用aclnnSparse4to2QuantMatmulWeightNz的链路如下:
- 准备满足"每 4 个元素至少 2 个零"的 INT8 权重矩阵
(n, k); - 调用 aclnnTransSparse4to2Para 压缩权重,获得
sparseWeight(含 StorageShape)与index(含 StorageShape); - 构造
x、xScale、sparseWeightScale、biasOptional、out等aclTensor,注意sparseWeight/index需以 view+storage 双 shape 方式创建; - 两段式调用
aclnnSparse4to2QuantMatmulWeightNzGetWorkspaceSize→ 申请 workspace →aclnnSparse4to2QuantMatmulWeightNz; - 同步 stream 并回读
out(BF16)。
对于算子整体能力、参数总览与更多约束(如不支持空 tensor、非连续 tensor),可进一步参阅 matmul/sparse4to2quant_matmul/README.md;仓库内的单元测试覆盖了 aclnn 接口、tiling 与 kernel 三个层面,分别位于 tests/ut 目录下(如 test_aclnn_sparse4to2quant_matmul.cpp 与对应 CSV 用例),可作为理解算子边界行为与自行扩展用例的参考。
- 人工智能
- 算子库
- 深度学习
- CANN
- Ascend
【免费下载链接】ops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
相关推荐
TileLang 稀疏矩阵乘法(T.gemm_sp)实践:从 2:4 结构化压缩到稀疏 Tensor Core GEMM
TileLang 稀疏矩阵乘法(T.gemm_sp)实践:从 2:4 结构化压缩到稀疏 Tensor Core GEMM 本文以 TileLang 官方教程 m
编译器编程语言高性能计算人工智能深度学习Faust生产环境部署:高可用、容错、扩展性保障
Faust生产环境部署:高可用、容错、扩展性保障 Faust是一个强大的Python流处理框架,基于Kafka构建,为实时数据处理提供了高效可靠的解决方案。本文
人工智能算子库深度学习CANNAscendCANN ops-nn 伪量化矩阵乘算子 aclnnWeightQuantBatchMatmulV2 使用指南
CANN ops nn 伪量化矩阵乘算子 aclnnWeightQuantBatchMatmulV2 使用指南 本文基于 CANN ops nn 开源算子库(
人工智能算子库深度学习CANNAscend
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考