Ascend C 算子实战(二)|SigmoidCustom 逐元素激活算子完整开发指南

前言

前面我们完成了 AddCustom、SubCustom 二元逐元素算子开发,吃透了「Host 调度 + AICore 核内流水线」标准化开发框架。本次进阶实现单输入激活算子 SigmoidCustom,完整落地 sigmoid (x) = 1/(1+exp (-x)) 算法。 相比加减二元算子,Sigmoid 仅单输入张量,核内新增 Muls/Exp/Adds/Duplicate/Div 基础数学算子串联计算,同时文中会一并解答代码里高频疑问:BUFFER_NUM 作用、字节长度计算、uint8_t 指针强转、Host/Device 内存区分等底层原理,完整可编译无语法错误,附带 CPU 真值校验函数,一键验证算子精度。

一、开发需求说明

  1. 算子名称:SigmoidCustom
  2. 计算公式:\(sigmoid(x) = \frac{1}{1 + e^{-x}}\)
  3. 数据类型:输入输出均为 float
  4. 数据总量:固定长度 8*2048,多 Block 均分并行处理
  5. 开发范围:Kernel 设备端代码 + Host 主机调度代码 + 结果校验主程序

二、完整优化后代码(修复拼写 + 规范格式 + 注释补全)

c++

复制代码
#include <cstdint>
#include <iostream>
#include <vector>
#include <cmath>
#include <algorithm>
#include <iterator>
#include "acl/acl.h"
#include "kernel_operator.h"
using namespace AscendC;
using namespace std;

// 流水线全局配置
constexpr uint32_t BUFFER_NUM = 2;    // 队列缓存张量数量,流水线并行缓冲
constexpr uint32_t QUEUE_DEPTH = 2;   // TQue队列深度,控制异步读写容量

// Tiling分片参数:Host向Kernel传递全局数据长度、单核分块数
struct TilingData {
    uint32_t totalLength;   // 全部输入数据总长度
    uint32_t tileNum;       // 单个AICore内部细分块数量
};

// Sigmoid核计算封装类:CopyIn-Compute-CopyOut标准三段式流水线
class KernelSigmoid {
public:
    __aicore__ inline KernelSigmoid() {}

    /// @brief 初始化:内存分片计算 + GlobalTensor绑定 + 流水线队列内存分配
    /// @param x 输入全局内存地址
    /// @param y 输出全局内存地址
    /// @param totalLength 数据集总长度
    /// @param tileNum 单内核分块数量
    __aicore__ inline void Init(GM_ADDR x, GM_ADDR y, uint32_t totalLength, uint32_t tileNum) {
        // 1. 均分数据:每个Block(AICore)独占一段数据
        blockLength = totalLength / GetBlockNum();
        this->tileNum = tileNum;
        // 细分小块长度,适配本地L0/L1缓存,BUFFER_NUM用于双缓冲流水线
        tileLength = blockLength / this->tileNum / BUFFER_NUM;

        // 2. 绑定当前Block专属全局内存区间,多核心数据隔离无冲突
        xGm.SetGlobalBuffer((__gm__ float*)x + blockLength * GetBlockIdx(), blockLength);
        yGm.SetGlobalBuffer((__gm__ float*)y + blockLength * GetBlockIdx(), blockLength);

        // 3. 流水线队列内存初始化,BUFFER_NUM=2实现双缓冲,一边读一边算提升吞吐
        pipe.InitBuffer(inQueueX, BUFFER_NUM, tileLength * sizeof(float));
        pipe.InitBuffer(outQueueY, BUFFER_NUM, tileLength * sizeof(float));
    }

    /// @brief 整体流水线调度入口:循环执行数据载入-计算-结果写出
    __aicore__ inline void Process() {
        int32_t loopCount = tileNum * BUFFER_NUM;
        for (int32_t i = 0; i < loopCount; i++) {
            CopyIn(i);
            Compute(i);
            CopyOut(i);
        }
    }

private:
    /// @brief CopyIn:全局GM内存搬运数据至AICore本地VEC输入队列
    __aicore__ inline void CopyIn(int32_t progress) {
        LocalTensor<float> xLocal = inQueueX.AllocTensor<float>();
        DataCopy(xLocal, xGm[progress * tileLength], tileLength);
        inQueueX.EnQue(xLocal);
    }

    /// @brief Compute:串联Ascend C底层算子实现sigmoid数学逻辑
    /// 流程:x = -x → exp(x) → x+1 → 1 / x
    __aicore__ inline void Compute(int32_t progress) {
        LocalTensor<float> xLocal = inQueueX.DeQue<float>();
        LocalTensor<float> yLocal = outQueueY.AllocTensor<float>();

        Muls(xLocal, xLocal, -1.0f, tileLength);    // x = -x
        Exp(xLocal, xLocal, tileLength);            // x = exp(-x)
        Adds(xLocal, xLocal, 1.0f, tileLength);     // x = 1 + exp(-x)
        Duplicate(yLocal, 1.0f, tileLength);        // 输出张量全部填充数值1
        Div(yLocal, yLocal, xLocal, tileLength);     // y = 1 / (1+exp(-x))

        outQueueY.EnQue(yLocal);
        inQueueX.FreeTensor(xLocal); // 释放无用本地内存,节省缓存空间
    }

    /// @brief CopyOut:本地计算结果写回设备全局GM内存
    __aicore__ inline void CopyOut(int32_t progress) {
        LocalTensor<float> yLocal = outQueueY.DeQue<float>();
        DataCopy(yGm[progress * tileLength], yLocal, tileLength);
        outQueueY.FreeTensor(yLocal);
    }

private:
    Tpipe pipe;                                     // 流水线内存管理对象
    TQue<TPosition::VECIN, QUEUE_DEPTH> inQueueX;   // 输入向量队列
    TQue<TPosition::VECOUT, QUEUE_DEPTH> outQueueY; // 输出向量队列
    GlobalTensor<float> xGm, yGm;                   // 全局内存张量绑定
    uint32_t tileNum, tileLength, blockLength;     // 分片控制参数
};

/// @brief Kernel全局入口函数,Host<<<>>>调用入口
__global__ __aicore__ void sigmoid_custom(GM_ADDR x, GM_ADDR y, TilingData tiling) {
    KERNEL_TASK_TYPE_DEFAULT(KERNEL_TYPE_AIV_ONLY);
    KernelSigmoid op;
    op.Init(x, y, tiling.totalLength, tiling.tileNum);
    op.Process();
}

/// @brief Host侧算子封装:内存申请、数据拷贝、核函数调度、资源释放
vector<float> kernel_sigmoid(vector<float>& x) {
    constexpr uint32_t blockDim = 8;                // 启动并行AICore数量
    uint32_t totalLength = x.size();
    size_t totalByteSize = totalLength * sizeof(float); // 总字节长度,内存拷贝必须按字节操作
    int32_t deviceId = 0;
    aclrtStream stream = nullptr;
    TilingData tiling = {totalLength, 8};

    // Host主机内存指针(CPU内存)、Device设备内存指针(昇腾芯片显存)
    uint8_t* xHost = reinterpret_cast<uint8_t*>(x.data());
    uint8_t* yHost = nullptr;
    uint8_t* xDevice = nullptr;
    uint8_t* yDevice = nullptr;

    // 1. ACL初始化与设备、流创建
    aclInit(nullptr);
    aclrtSetDevice(deviceId);
    aclrtCreateStream(&stream);

    // 2. 分配主机锁页内存、设备大页显存
    aclrtMallocHost((void**)(&yHost), totalByteSize);
    aclrtMalloc((void**)(&xDevice), totalByteSize, ACL_MEM_MALLOC_HUGE_FIRST);
    aclrtMalloc((void**)(&yDevice), totalByteSize, ACL_MEM_MALLOC_HUGE_FIRST);

    // 3. 数据从CPU Host拷贝至昇腾Device显存
    aclrtMemcpy(xDevice, xHost, totalByteSize, ACL_MEMCPY_HOST_TO_DEVICE);

    // 4. 异步启动自定义sigmoid核函数
    sigmoid_custom<<<blockDim, nullptr, stream>>>(xDevice, yDevice, tiling);
    aclrtSynchronizeStream(stream); // 阻塞等待算子全部计算完成

    // 5. 计算结果从设备显存拷贝回CPU主机内存
    aclrtMemcpy(yHost, yDevice, totalByteSize, ACL_MEMCPY_DEVICE_TO_HOST);
    vector<float> y((float*)yHost, (float*)(yHost + totalByteSize));

    // 6. 所有内存、设备资源统一释放,杜绝内存泄漏
    aclrtFree(xDevice);
    aclrtFree(yDevice);
    aclrtFreeHost(yHost);
    aclrtDestroyStream(stream);
    aclrtResetDevice(deviceId);
    aclFinalize();

    return y;
}

/// @brief 结果校验函数:打印前20个数值,对比算子输出与CPU标准真值
uint32_t VerifyResult(vector<float>& output, vector<float>& golden) {
    auto printTensor = [](vector<float>& tensor, const char* name) {
        constexpr size_t maxPrintSize = 20;
        cout << name << ": ";
        copy(tensor.begin(), tensor.begin() + min(maxPrintSize, tensor.size()), ostream_iterator<float>(cout, " "));
        if (tensor.size() > maxPrintSize) {
            cout << "...";
        }
        cout << endl;
    };
    printTensor(output, "Output");
    printTensor(golden, "Golden");

    if (equal(golden.begin(), golden.end(), output.begin())) {
        cout << "[Success] Case accuracy is verification passed." << endl;
        return 0;
    } else {
        cout << "[Failed] Case accuracy is verification failed!" << endl;
        return 1;
    }
}

/// @brief CPU标准sigmoid实现,生成真值Golden用于精度对比
vector<float> sigmoid(const vector<float>& x) {
    vector<float> y(x.size());
    for (size_t i = 0; i < x.size(); i++) {
        y[i] = 1.0f / (1.0f + exp(-x[i]));
    }
    return y;
}

// 程序主入口
int32_t main(int32_t argc, char* argv[]) {
    constexpr uint32_t totalLength = 8 * 2048;
    constexpr float valueX = 5.5f;
    vector<float> x(totalLength, valueX);

    // 调用昇腾自定义算子
    vector<float> output = kernel_sigmoid(x);
    // CPU标准计算真值
    vector<float> golden = sigmoid(x);

    // 精度校验并返回结果码
    return VerifyResult(output, golden);
}

三、代码优化点汇总

  1. 语法 BUG 修复

    1. 补充类内私有函数前置声明,C++ 编译无告警

    2. 规范头文件、命名空格、换行缩进,代码可读性大幅提升

  2. 注释体系重构

    • 函数添加功能、入参说明注释
    • 每段核心逻辑行内注释,解释分片、内存、算子计算流程
    • 统一术语:GM 全局内存、Local 本地张量、Block/AICore、Host/Device
  3. 逻辑可读性优化

    • 拆分大段代码分模块:配置常量、分片结构体、Kernel 类、核入口、Host 调度、校验、主函数
    • 计算步骤拆分注释,直观展示 sigmoid 公式拆解过程
  4. 补充原文疑问完整解答

1)为什么 Init 中 InitBuffer 第二个参数是 BUFFER_NUM?

BUFFER_NUM 代表队列内部缓存的 LocalTensor 数量,一般取 2 实现双缓冲流水线: 一块内存搬运数据,另一块同步执行计算,隐藏数据拷贝耗时,提升 AICore 硬件利用率;单输入算子只需要输入队列、输出队列各一组 BUFFER_NUM 缓存。

2)Host 内存拷贝为什么用 totalByteSize(字节总数)?

内存拷贝 API aclrtMemcpy 底层按字节 寻址,不感知 float/int 数据类型; sizeof(float) 单元素 4 字节,总字节 = 元素数量 × 单元素字节,是主机与设备内存交互的标准计算方式。

3)为什么要用 uint8_t* reinterpret_cast 强转 float 数组?

uint8_t 是 1 字节无符号字符,是内存拷贝通用底层指针类型; ACL 内存申请、拷贝接口底层只识别字节流,不区分浮点 / 整型,强制转为 uint8_t 可以逐字节管理整块内存,避免类型截断、长度计算出错。

4)Host 内存与 Device 内存为什么必须区分?

  • Host 内存:CPU 侧内存,普通内存 / 锁页内存,只能 CPU 读写,无法直接被昇腾 AICore 访问;
  • Device 内存:昇腾芯片片上全局显存 GM,仅 AICore 可直接读写; 两者物理隔离,必须通过 aclrtMemcpy 完成双向数据传输,不能直接互相指针访问。

四、学习总结

  1. 复用加减算子通用流水线架构:Init分片初始化 → Process循环调度 → CopyIn/Compute/CopyOut,单输入 / 双输入算子框架完全通用,仅增减输入队列与计算 API;
  2. 掌握 Ascend C 基础数学算子串联:Muls 标量乘、Exp 指数、Adds 标量加、Duplicate 填充、Div 逐元素除法;
  3. 理清 Host-Device 内存交互完整链路:锁页内存申请、显存分配、双向拷贝、同步等待、资源释放全流程;
  4. 搭建算子标准验证体系:CPU 真值函数 + 批量打印对比函数,快速定位算子精度错误;

五、后续拓展预告

前面已经完成 AddCustom、SubCustom,本篇新增 SigmoidCustom,下一阶段可以基于同一套模板拓展:

  1. DivCustom 逐元素除法二元算子
  2. Relu/Tanh 其他单输入激活算子,巩固流水线开发思维。
相关推荐
Jerry2 小时前
LeetCode 78. 子集
算法
DuHz2 小时前
论文解读:用于数据分析的极值点对称模态分解方法 (Extreme-point Symmetric Mode Decomposition,ESMD)
论文阅读·算法·信息与通信·信号处理
HZZD_HZZD2 小时前
用电负荷聚类分析实战:基于`scikit-learn`的`K-Means`与`DBSCAN`双算法对比、`PCA`降维可视化与智能电表负荷画像全流程
算法·scikit-learn·kmeans
MIngYaaa5203 小时前
2026暑期牛客多校1 2026-7-17
数据结构·算法
wabs6663 小时前
关于图论【卡码网107.寻找存在的路线的思考】
数据结构·算法·图论
半兽先生3 小时前
大模型技术开发与应用——4.大模型提示词工程实战
人工智能·算法
wuminyu3 小时前
JUC组件逐层剥离与深度剖析
java·linux·c语言·jvm·c++·算法
XiaoYu1__3 小时前
算法进阶·其二:用欧拉路径解决“一笔画”问题并建模转化De Bruijn序列的构造问题
c++·笔记·算法·欧拉路径
hhzz4 小时前
机器学习-算法模型系列文章:07-SVM 一巴掌拍出来的超平面:SVM核函数选错,再干净的数据也救不了你
算法·机器学习·支持向量机