CANN ops-transformer 算子 aclnnAttentionWorkerCombine 详解:Attention/FFN 分离部署下的 Token 融合加权算子
【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer
本技术指南围绕 CANN ops-transformer 仓库中的 aclnnAttentionWorkerCombine 算子文档展开,完整讲解该算子的功能定位、两段式接口用法、参数语义、约束条件与可运行的调用示例,并结合仓库源码(算子定义、shape/dtype 推导、tiling 与 kernel 实现)深入剖析其底层原理。读者读完可以掌握在 Attention 与 FFN 分离部署场景下,如何通过 aclnn 接口完成注意力 token 数据的加权融合与层 ID 更新,并将其接入 AttentionWorkerScheduler 等算子组成的完整工作流。
功能概述:Attention 侧的数据融合算子
AttentionWorkerCombine是 CANN ops-transformer 中面向Attention 与 FFN 分离部署场景的 Attention 侧数据融合算子。当大模型以流水线方式将 Attention 计算单元与 FFN 计算单元拆分到不同设备/核上部署时,Attention 侧往往需要接收 FFN 侧回传的、由多个计算单元分别处理产生的注意力 token 数据。该算子负责:
- 接收 FFN 侧回传的数据:数据以
ScheduleContext结构体内存排布方式存储。该结构体包含CommonArea、ControlArea、AttentionArea、FfnArea四个域,算子从AttentionArea的token_data_buf中读取 token 数据。 - 加权融合:结合专家权重
expertScales对多路 token 数据进行加权求和,输出最终的注意力融合结果y。 - 更新层 ID:根据输入
layerId计算并输出nextLayerId,指示下一个要处理的层 ID。
计算公式如下:
$$ y[i] = \sum_{k=0}^{K-1} \text{expertScales}[i][k] \times \text{token_data}[i][k] $$
$$ \text{nextLayerId} = \text{layerId} + 1 $$
其中K为每个 token 对应的专家数(即expertScales的第二维大小),i遍历整个 batch 的 token。
重要提示:该算子不建议单独使用,建议与
AttentionWorkerScheduler等算子配合使用,形成完整的工作流。AttentionWorkerScheduler负责扫描并确认 FFN 侧数据是否准备就绪(通过轮询AttentionArea.token_info_buf中的 flag),而AttentionWorkerCombine在数据就绪后完成读取、融合与输出,两者共同构成 Attention 侧完整的数据接收-消费链路。相关算子说明见 attention_worker_scheduler/README.md。
产品支持情况
AttentionWorkerCombine在不同 NPU 产品上的支持情况如下(与仓库中算子定义op_host/config下的 aicore 配置对应):
| 产品 | 是否支持 |
|---|---|
| Ascend 950PR / Ascend 950DT | 不支持 |
| Atlas A3 训练系列产品 / Atlas A3 推理系列产品 | 支持 |
| Atlas A2 训练系列产品 / Atlas A2 推理系列产品 | 支持 |
| Atlas 200I/500 A2 推理产品 | 不支持 |
| Atlas 推理系列产品 | 不支持 |
| Atlas 训练系列产品 | 不支持 |
从源码角度看,attention_worker_combine_def.cpp 中通过this->AICore().AddConfig("ascend910_93")与this->AICore().AddConfig("ascend910b")注册了 aicore 配置,与文档中"Atlas A3 / A2 系列支持"的产品矩阵一致。算子对应不同产品的配置文件位于 op_host/config/ascend910_93 与 op_host/config/ascend910b 目录下(各含attention_worker_combine.json与attention_worker_combine_simplified_key.ini)。
两段式接口与函数原型
每个 aclnn 算子都采用两段式接口:必须先调用aclnnAttentionWorkerCombineGetWorkspaceSize接口获取计算所需 workspace 大小以及包含了算子计算流程的执行器,再调用aclnnAttentionWorkerCombine接口执行计算。两段式接口的通用机制可参考 两段式接口说明。
第一段接口原型:
aclnnStatus aclnnAttentionWorkerCombineGetWorkspaceSize( const aclTensor *scheduleContext, const aclTensor *expertScales, const aclTensor *layerId, int64_t hiddenSize, int64_t tokenDtype, int64_t needSchedule, const aclTensor *y, const aclTensor *nextLayerId, uint64_t *workspaceSize, aclOpExecutor **executor)第二段接口原型:
aclnnStatus aclnnAttentionWorkerCombine( void* workspace, uint64_t workspaceSize, aclOpExecutor* executor, const aclrtStream stream)第一段接口负责入参校验、shape/dtype 推导与 tiling 计算,并将执行流程封装进executor;第二段接口在指定stream上真正下发算子任务。
aclnnAttentionWorkerCombineGetWorkspaceSize 参数说明
参数表
| 参数名 | 输入/输出 | 描述 | 使用说明 | 数据类型 | 数据格式 | 维度(shape) | 非连续Tensor |
|---|---|---|---|---|---|---|---|
| scheduleContext | 输入 | Attention 侧接收的调度上下文,内含 CommonArea、ControlArea、AttentionArea、FfnArea。算子从 AttentionArea 的 token_data_buf 中读取 token 数据。 | 不支持空 tensor。 | INT8 | ND | 1维,shape 固定为 (1024) | × |
| expertScales | 输入 | 专家权重,表示每个 token 对应的各专家权重。 | - | FLOAT | ND | 2维,(BS, K) | × |
| layerId | 输入 | 当前的模型层 ID。 | - | INT32 | ND | 1维,(1) | × |
| hiddenSize | 输入 | token_data 的隐藏维度大小,用于确定输出 y 的第二维大小。 | - | INT64 | - | - | - |
| tokenDtype | 输入 | 指定 scheduleContext 中 token 数据的原始精度类型。0 表示 FLOAT16;1 表示 BFLOAT16。 | 取值为 0 或 1。 | INT64 | - | - | - |
| needSchedule | 输入 | 指定是否等待 token 数据填充完成后再执行。0 表示不等待;1 表示等待。 | 取值为 0 或 1。 | INT64 | - | - | - |
| y | 输出 | 最终的注意力合并结果。 | - | FP16、BF16 | ND | 2维,(BS, hiddenSize) | × |
| nextLayerId | 输出 | 下一个要处理的层 ID。 | - | INT32 | ND | 1维,(1) | × |
| workspaceSize | 输出 | 返回需要在 Device 侧申请的 workspace 大小。 | - | - | - | - | - |
| executor | 输出 | 返回 op 执行器,包含了算子计算流程。 | - | - | - | - | - |
几个参数需要注意:
scheduleContext是1024 字节的固定长度 INT8 张量,本质上是ScheduleContext结构体在 Device 侧的字节镜像(见下文"调用示例"中的结构体定义与static_assert(sizeof(ScheduleContext) == 1024))。expertScales的 shape 为(BS, K),其中BS为 batch size,K为每个 token 的专家数;它决定了输出y的第一维大小,而y的第二维由hiddenSize决定。这一点与 attention_worker_combine_infershape.cpp 中的InferShape4AttentionWorkerCombine实现完全一致:yShape->SetDim(0, expertScalesInputShape->GetDim(0))、yShape->SetDim(1, *hiddenSize)。tokenDtype同时决定输出y的数据类型:由 InferDtype4AttentionWorkerCombine 可知,tokenDtype == 1(BF16)时y为DT_BF16,否则为DT_FLOAT16;nextLayerId的类型直接继承输入layerId的类型。
返回值
两段接口均返回aclnnStatus状态码,具体参见 aclnn 返回码。
第一段接口会完成入参校验,出现以下场景时报错:
| 返回值 | 错误码 | 描述 |
|---|---|---|
| ACLNN_ERR_PARAM_NULLPTR | 161001 | 输入是空指针。 |
| ACLNN_ERR_PARAM_INVALID | 161002 | 输入数据类型不在支持的范围内。 |
aclnnAttentionWorkerCombine 参数说明
第二段接口的参数相对简单,均为执行阶段所需的运行时资源:
| 参数名 | 输入/输出 | 描述 |
|---|---|---|
| workspace | 输入 | 在 Device 侧申请的 workspace 内存地址。 |
| workspaceSize | 输入 | 在 Device 侧申请的 workspace 大小,由第一段接口aclnnAttentionWorkerCombineGetWorkspaceSize获取。 |
| executor | 输入 | op 执行器,包含了算子计算流程。 |
| stream | 输入 | 指定执行任务的 Stream。 |
返回值同样为aclnnStatus状态码,参见 aclnn 返回码。
约束说明
expertScales的第二维 K ≤ 64。该限制在 tiling 阶段被强制执行:见 attention_worker_combine_tiling.cpp 中的OP_CHECK_IF(k > K_UPPER_BOUND, ...)校验(K_UPPER_BOUND = 64)。- 确定性计算:
aclnnAttentionWorkerCombine为默认确定性实现。 scheduleContext为 1D 张量,shape 固定为(1024)。expertScales为 2D 张量[BatchSize, K];y为 2D 张量[BatchSize, HiddenSize];layerId与nextLayerId均为 1D 张量。
调用示例
以下示例代码演示了完整的调用流程(该示例同样保存在仓库 examples/test_aclnn_attention_worker_combine.cpp 中,可直接对照阅读)。具体编译和执行过程请参考 编译与运行样例。
1. 头文件与辅助宏
#include <iostream> #include <vector> #include <cstring> #include "acl/acl.h" #include "aclnnop/aclnn_attention_worker_combine.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)2. 辅助函数:初始化、环境释放与张量创建
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; } void Finalize(int32_t deviceId, aclrtStream stream) { aclrtDestroyStream(stream); aclrtResetDevice(deviceId); aclFinalize(); } 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); 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); 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); std::vector<int64_t> stride(shape.size(), 1); for (int64_t i = shape.size() - 2; i >= 0; i--) { stride[i] = shape[i + 1] * stride[i + 1]; } *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, stride.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; } int CreateAclTensorNoData(const std::vector<int64_t>& shape, void** deviceAddr, aclDataType dataType, aclTensor** tensor) { uint64_t elemSize = sizeof(int8_t); if (dataType == ACL_INT32) { elemSize = sizeof(int32_t); } if (dataType == ACL_FLOAT16) { elemSize = sizeof(int16_t); } if (dataType == ACL_BF16) { elemSize = sizeof(int16_t); } auto size = GetShapeSize(shape) * elemSize; 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); std::vector<int64_t> stride(shape.size(), 1); for (int64_t i = shape.size() - 2; i >= 0; i--) { stride[i] = shape[i + 1] * stride[i + 1]; } *tensor = aclCreateTensor(shape.data(), shape.size(), dataType, stride.data(), 0, aclFormat::ACL_FORMAT_ND, shape.data(), shape.size(), *deviceAddr); return 0; }3. ScheduleContext 结构体定义(内存布局)
ScheduleContext是算子与调度工作流之间的"契约":它按#pragma pack(push, 1)紧凑排布,总大小固定为1024 字节,通过static_assert强制校验。四个域分别承担不同职责:
CommonArea:全局配置,如session_num(Attention 节点数)、micro_batch_num(batch 拆分份数)、micro_batch_size、selected_expert_num(topK + 1)、expert_num(每层专家数)、attn_to_ffn_token_size/ffn_to_attn_token_size(token 在对方 window 数据区的存储占用)、schedule_mode(0 仅调度 FFN,1 仅调度 Attention,2 同时调度)。ControlArea:run_flag控制循环退出(0 退出,1 运行中)。AttentionArea:核心数据缓冲区与状态信息。token_info_buf存放每个 micro batch 的DataDesc(内含每个 token×专家对应的就绪 flag);token_data_buf存放[M, BS, K+1, HS]的 token 数据;micro_batch_id记录最新就绪的 micro batch id。FfnArea:FFN 侧回传相关的缓冲区指针与out_num等状态。
#pragma pack(push, 1) struct AttentionDataDesc { int32_t flag[0]; }; struct ScheduleContext { struct CommonArea { uint32_t session_num; // Number of attention nodes uint32_t micro_batch_num; uint32_t micro_batch_size; uint32_t selected_expert_num; // topK + 1 uint32_t expert_num; // experts per layer uint32_t attn_to_ffn_token_size; uint32_t ffn_to_attn_token_size; int32_t schedule_mode; // 0: Ffn only 1: Attention only int8_t reserve0[96]; }; struct ControlArea { int32_t run_flag; // 0 : exited 1 : running int8_t reserve2[124]; }; struct AttentionArea { uint64_t token_info_buf; // Points to device memory. uint64_t token_info_buf_size; uint64_t token_data_buf; // Points to device memory. uint64_t token_data_buf_size; uint32_t micro_batch_id; // Records the latest ready micro batch id. int8_t reserve5[92]; }; struct FfnArea { uint64_t token_info_buf; uint64_t token_info_buf_size; uint64_t token_data_buf; uint64_t token_data_buf_size; uint64_t polling_index; int8_t reserve3[88]; uint64_t layer_ids_buf; uint64_t layer_ids_buf_size; uint64_t session_ids_buf; uint64_t session_ids_buf_size; uint64_t micro_batch_ids_buf; uint64_t micro_batch_ids_buf_size; uint64_t expert_ids_buf; uint64_t expert_ids_buf_size; uint32_t out_num; int8_t reserve4[60]; }; CommonArea common; ControlArea control; AttentionArea attention; FfnArea ffn; int8_t reserve6[384]; // Padding to 1024 bytes. }; static_assert(sizeof(ScheduleContext) == 1024, "ScheduleContext size must be 1024 bytes"); #pragma pack(pop)该结构与 kernel 侧的定义严格对齐:见 attention_worker_combine_common_utils.h,kernel 通过GET_OFFSET_B32/GET_OFFSET_B64宏按成员偏移直接读取schedule_context中的字段,因此 Host 侧结构体的字段顺序与 padding 必须与 kernel 侧保持一致。
4. main 函数:完整调用流程
int main() { 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的接口自定义构造 int64_t BS = 48; // batch size int64_t K = 8; // expert num per token int64_t hiddenSize = 20480; int64_t tokenDtype = 1; // BF16 int64_t needSchedule = 0; // 初始化ScheduleContext ScheduleContext scheduleContext = {}; scheduleContext.common.session_num = 1; scheduleContext.common.micro_batch_num = 1; scheduleContext.common.micro_batch_size = BS; scheduleContext.common.selected_expert_num = K; scheduleContext.common.expert_num = 16; scheduleContext.common.attn_to_ffn_token_size = 512; scheduleContext.common.ffn_to_attn_token_size = 512; scheduleContext.common.schedule_mode = 1; // Attention only scheduleContext.control.run_flag = 1; // running scheduleContext.attention.micro_batch_id = 0; // 初始化Attention token_info_buf(flag置1表示数据就绪) size_t perDataDescSize = sizeof(AttentionDataDesc) + sizeof(int32_t) * BS * K; size_t tokenInfoBufSize = static_cast<size_t>(scheduleContext.common.micro_batch_num) * perDataDescSize; void* tokenInfoBuf = nullptr; ret = aclrtMalloc(&tokenInfoBuf, tokenInfoBufSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("malloc token info buf failed. ERROR: %d\n", ret); return ret); scheduleContext.attention.token_info_buf = reinterpret_cast<uint64_t>(tokenInfoBuf); scheduleContext.attention.token_info_buf_size = tokenInfoBufSize; std::vector<int32_t> hostFlags(static_cast<size_t>(BS) * K, 1); ret = aclrtMemcpy(tokenInfoBuf, tokenInfoBufSize, hostFlags.data(), static_cast<size_t>(BS) * K * sizeof(int32_t), ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("cpy token info buf failed. ERROR: %d\n", ret); return ret); // 初始化Attention token_data_buf uint64_t tokenDataSize = static_cast<uint64_t>(BS) * K * hiddenSize * sizeof(int16_t); void* tokenDataBuf = nullptr; ret = aclrtMalloc(&tokenDataBuf, tokenDataSize, ACL_MEM_MALLOC_HUGE_FIRST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("malloc token data buf failed. ERROR: %d\n", ret); return ret); scheduleContext.attention.token_data_buf = reinterpret_cast<uint64_t>(tokenDataBuf); scheduleContext.attention.token_data_buf_size = tokenDataSize; std::vector<int16_t> hostTokenData(static_cast<size_t>(BS) * K * hiddenSize, 1); ret = aclrtMemcpy(tokenDataBuf, tokenDataSize, hostTokenData.data(), tokenDataSize, ACL_MEMCPY_HOST_TO_DEVICE); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("cpy token data buf failed. ERROR: %d\n", ret); return ret); // 创建scheduleContext aclTensor std::vector<int64_t> scheduleContextShape = {1024}; void* scheduleContextDeviceAddr = nullptr; aclTensor* scheduleContextRef = nullptr; std::vector<int8_t> hostCtxData(1024, 0); std::memcpy(hostCtxData.data(), &scheduleContext, sizeof(ScheduleContext)); ret = CreateAclTensor(hostCtxData, scheduleContextShape, &scheduleContextDeviceAddr, aclDataType::ACL_INT8, &scheduleContextRef); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建expert_scales aclTensor, shape (BS, K) std::vector<int64_t> expertScalesShape = {BS, K}; std::vector<float> hostExpertScales(static_cast<size_t>(BS) * K, 0.125f); void* expertScalesDeviceAddr = nullptr; aclTensor* expertScalesRef = nullptr; ret = CreateAclTensor(hostExpertScales, expertScalesShape, &expertScalesDeviceAddr, aclDataType::ACL_FLOAT, &expertScalesRef); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建layer_id aclTensor, shape (1) std::vector<int64_t> layerIdShape = {1}; std::vector<int32_t> hostLayerId = {0}; void* layerIdDeviceAddr = nullptr; aclTensor* layerIdRef = nullptr; ret = CreateAclTensor(hostLayerId, layerIdShape, &layerIdDeviceAddr, aclDataType::ACL_INT32, &layerIdRef); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建输出y aclTensor, shape (BS, hiddenSize) std::vector<int64_t> yShape = {BS, hiddenSize}; void* yDeviceAddr = nullptr; aclTensor* yRef = nullptr; ret = CreateAclTensorNoData(yShape, &yDeviceAddr, aclDataType::ACL_BF16, &yRef); CHECK_RET(ret == ACL_SUCCESS, return ret); // 创建输出next_layer_id aclTensor, shape (1) std::vector<int64_t> nextLayerIdShape = {1}; void* nextLayerIdDeviceAddr = nullptr; aclTensor* nextLayerIdRef = nullptr; ret = CreateAclTensorNoData(nextLayerIdShape, &nextLayerIdDeviceAddr, aclDataType::ACL_INT32, &nextLayerIdRef); CHECK_RET(ret == ACL_SUCCESS, return ret); // 3. 调用CANN算子库API uint64_t workspaceSize = 0; aclOpExecutor* executor = nullptr; ret = aclnnAttentionWorkerCombineGetWorkspaceSize(scheduleContextRef, expertScalesRef, layerIdRef, hiddenSize, tokenDtype, needSchedule, yRef, nextLayerIdRef, &workspaceSize, &executor); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnAttentionWorkerCombineGetWorkspaceSize failed. ERROR: %d\n", ret); return ret); void* workspaceAddr = nullptr; 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 = aclnnAttentionWorkerCombine(workspaceAddr, workspaceSize, executor, stream); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("aclnnAttentionWorkerCombine 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侧 int32_t nextLayerId = 0; ret = aclrtMemcpy(&nextLayerId, sizeof(int32_t), nextLayerIdDeviceAddr, sizeof(int32_t), ACL_MEMCPY_DEVICE_TO_HOST); CHECK_RET(ret == ACL_SUCCESS, LOG_PRINT("copy next_layer_id failed. ERROR: %d\n", ret); return ret); LOG_PRINT("next_layer_id = %d.\n", nextLayerId); // 6. 释放aclTensor aclDestroyTensor(scheduleContextRef); aclDestroyTensor(expertScalesRef); aclDestroyTensor(layerIdRef); aclDestroyTensor(yRef); aclDestroyTensor(nextLayerIdRef); // 7. 释放device资源 aclrtFree(scheduleContextDeviceAddr); aclrtFree(expertScalesDeviceAddr); aclrtFree(layerIdDeviceAddr); aclrtFree(yDeviceAddr); aclrtFree(nextLayerIdDeviceAddr); aclrtFree(tokenInfoBuf); aclrtFree(tokenDataBuf); if (workspaceSize > 0) { aclrtFree(workspaceAddr); } Finalize(deviceId, stream); return 0; }示例中BS=48、K=8、hiddenSize=20480、tokenDtype=1(BF16)、needSchedule=0(不等待,因为示例中数据已由 Host 预先填充)。若实际工作流中数据由 FFN 侧异步回传,应将needSchedule置为 1,算子会轮询token_info_buf中的 flag 直到数据全部就绪。
源码级实现解析
算子定义(op_def)
attention_worker_combine_def.cpp 中通过OpDef注册了算子的输入输出与属性:
- 输入:
schedule_context(INT8)、expert_scales(FLOAT)、layer_id(INT32),均要求 ND 格式并AutoContiguous; - 输出:
y(FLOAT16 / BF16)、next_layer_id(INT32); - 属性:
hidden_size(必选 Int)、token_dtype(可选 Int,默认 0)、need_schedule(可选 Int,默认 0)。
这与 aclnn 接口的形参一一对应:hiddenSize/tokenDtype/needSchedule三个int64_t参数即映射为上述三个属性。
Shape 与 Dtype 推导(infershape)
attention_worker_combine_infershape.cpp 完成了图模式下的推导逻辑:
y的 shape 为(expertScales[0], hiddenSize),即第一维取自expertScales的 BS,第二维取自属性hiddenSize;nextLayerId的 shape 与layerId相同([1]);y的数据类型由tokenDtype决定(1 → BF16,否则 → FP16);nextLayerId的类型继承layerId。
Tiling 策略(按 BS / H / K 三维切分)
attention_worker_combine_tiling.cpp 中的DoOpTiling根据 UB 容量与 batch 大小,在三种切分策略中选择并生成对应的 tiling key:
- BS 全载 + 按 BS 分核:当
hiddenSize对齐后不超过 UB 可容纳的hInFullK上限时,K 与 H 均整块载入,只对 BS 维按核数切分(tiling key10000/10001,FP16/BF16); - 按 H 分核:BS 分核后核数未达上限且 H 过大时,对 H 维切块、跨多核并行,并支持
needSchedule模式下的循环切块(tiling key10010/10011); - 按 K 切分:H 可整块容纳但 K 过大时,对 K 维分组循环处理(tiling key
10020/10021)。
每个 tiling key 在 attention_worker_combine.cpp 的 kernel 入口中分派到对应的模板实现:KernelAttentionWorkerCombineSplitBS、KernelAttentionWorkerCombineSplitH、KernelAttentionWorkerCombineSplitK,分别以half(FP16)与bfloat16_t(BF16)实例化。此外,PostTiling中还会设置DEFAULT_WORKSPACE_SIZE = 32字节的 workspace。
Kernel 计算逻辑
以 attention_worker_combine_split_bs.h 的Process为例,其核心流程为:
- 按
GetBlockIdx()计算本核负责的 BS 区间(tailCoreBsLoopNum处理尾块); - block 0 额外计算
nextLayerId:dstNextLayerIdGm(0) = srcLayerIdGm(0) + 1,对应文档中的nextLayerId = layerId + 1; - 若
needSchedule == 1,通过ScanTokenInfo轮询token_info_buf中全部bsLoopNum * (K + 1)个 flag,累加就绪数,直到全部为 1 才继续(对应 Scheduler 工作流中的等待语义);计算完成后调用ClearTokenInfo将 flag 清零,并由 block 0 回写micro_batch_id; - 对每个 BS 循环:
CopyIn载入(K+1)路 token 数据 →Compute完成加权求和 →CopyOut写出y。
Compute(第 201-233 行)的实现正是公式 $y[i] = \sum_{k=0}^{K-1} \text{expertScales}[i][k] \times \text{token_data}[i][k]$ 的向量化展开:对前 K 路数据依次执行Cast(FP16/BF16 → FP32)→ 从expertScales取权重Muls缩放 →Add累加,最后加上第K路(K+1数据布局中索引为 K 的 slot,通常为 0 或占位数据),再Cast回输出精度写回y。
与 AttentionWorkerScheduler 组成的工作流
AttentionWorkerCombine是 Attention/FFN 分离部署工作流中的消费端算子,建议按以下链路使用:
- FFN 侧计算完成后,通过 FfnToAttention 类算子将 token 数据与就绪 flag 写入
ScheduleContext的 Attention 域缓冲区; - AttentionWorkerScheduler(见 attention_worker_scheduler/README.md)轮询
token_info_buf中的 flag,确认数据全部就绪,并维护micro_batch_id的推进; - AttentionWorkerCombine读取就绪的
token_data_buf,结合expertScales完成多路 token 的加权融合,输出y并更新nextLayerId,驱动模型进入下一层处理。
当needSchedule = 1时,AttentionWorkerCombine自身也会在 kernel 内部执行就绪 flag 的轮询与清零,从而与 Scheduler 形成双保险的同步机制;在显式调度(数据由上层确保就绪)的场景下可置needSchedule = 0跳过该开销。
总结
aclnnAttentionWorkerCombine是 CANN ops-transformer 中实现 Attention/FFN 分离部署下 token 融合的关键算子:它以固定 1024 字节的ScheduleContext为数据契约,从AttentionArea.token_data_buf读取多路注意力 token 数据,按expertScales加权求和输出y,并顺带完成层 ID 的推进。使用时应牢记两段式接口调用次序、scheduleContext固定 shape(1024)、K ≤ 64以及tokenDtype与输出精度的联动关系,并结合AttentionWorkerScheduler组成完整工作流。仓库中同时提供了完整的 aclnn 调用样例 test_aclnn_attention_worker_combine.cpp、图模式构图样例 test_geir_attention_worker_combine.cpp(对应算子 IR 定义 attention_worker_combine_proto.h)以及 Host 侧单测 test_attention_worker_combine_infershape.cpp 和 test_attention_worker_combine_tiling.cpp,可作为继续深入研究的起点。
【免费下载链接】ops-transformer本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。项目地址: https://gitcode.com/cann/ops-transformer
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考