前言

在大规模分布式训练和推理场景下,昇腾NPU(Ascend NPU)凭借其高算力密度和专用硬件架构,已经成为国产AI基础设施的重要选择。而CANN(Compute Architecture for Neural Networks)作为昇腾AI计算的核心软件栈,向下驱动硬件、向上支撑AI框架,承担着算子编译、调度、执行等关键职责。在CANN体系中,各类领域加速库(如Ascend Transformer Boost加速库、信号加速库等)需要一种统一的方式与底层算子交互,这就是ascend-boost-comm存在的意义——它是一个领域加速库公共组件,统一定义了算子调用的L0级接口,南向对接不同组织开发的算子库,北向支撑不同加速库应用,实现M乘N的算子能力复用。本文将从源码出发,带你一步步理解ascend-boost-comm的架构设计,并动手完成一个自定义算子的完整开发流程。

一、为什么需要Ascend Boost Comm

在昇腾软件生态中,算子的开发和调用是一个多层级的过程。从上到下大致可以分为:AI框架层(PyTorch、MindSpore等)、领域加速库层(ATB、信号处理库等)、算子调用接口层、以及底层算子实现层。在没有统一接口的情况下,每个加速库都要自己维护一套算子加载、调度、执行的逻辑,这不仅造成大量重复代码,还导致算子无法在不同加速库之间复用。

Ascend Boost Comm解决的核心问题就是"算子调用的标准化"。它提供了一套完整的L0级接口规范,包含算子注册、参数传递、张量描述、内核调度、tiling计算等关键能力的标准实现。当一个算子开发者按照这套规范编写算子后,该算子可以被ATB加速库使用,也可以被信号加速库使用,甚至可以集成到其他自定义的加速库中。这就是M乘N复用的含义:M个加速库共享N个算子,而不需要为每对组合单独开发。

从代码结构上看,ascend-boost-comm仓库的核心源码集中在src目录下,其中schedule子模块负责算子调度逻辑,mki_loader负责算子的动态加载,utils提供各类辅助工具。头文件通过mki命名空间暴露统一API,上层加速库只需include对应的头文件即可使用。

支持Ascend Boost Comm的硬件平台包括Atlas A2训练/推理系列、Atlas A3训练/推理系列以及Atlas推理系列产品,覆盖了当前昇腾计算的主要硬件形态。

二、环境准备与工程搭建

在开始实际开发之前,需要先准备好编译和运行环境。以下是关键依赖说明。

2.1 基础编译依赖

ascend-boost-comm本身的编译只需要三样东西:Python 3.10或3.11、cmake 3.20及以上、gcc/g++ 7.3.1到11.x版本。注意,如果使用GCC 12或更高版本,需要在编译时加上–no_werror参数,因为PyTorch等第三方头文件在新版GCC下会触发一些警告,而这些警告在默认的-Werror配置下会被当作编译错误。

运行示例和测试用例时,则需要额外安装PyTorch 2.1.0及以上版本以及配套的torch_npu扩展包。torch_npu的版本必须与CANN版本匹配,安装前需要查阅昇腾官方文档确认版本对应关系。举例来说,如果使用CANN 9.0.0和PyTorch 2.7.1,可以这样安装:

pip install torch==2.7.1 torch-npu==2.7.1.post4

2.2 CANN Toolkit安装

CANN是昇腾计算的底层软件包,所有基于昇腾NPU的开发都必须先安装CANN Toolkit。安装命令很简单:

chmod +x Ascend-cann-toolkit_${VERSION}_linux-$(arch).run
./Ascend-cann-toolkit_${VERSION}_linux-$(arch).run --install

安装完成后需要配置环境变量,假设安装路径在${HOME}/Ascend:

source ${HOME}/Ascend/ascend-toolkit/set_env.sh

同时还需要安装一些Python依赖库,这些库在运行CANN相关工具时会用到:

pip3 install attrs cython 'numpy>=1.19.2,<=1.24.0' decorator sympy cffi pyyaml pathlib2 psutil protobuf==3.20.0 'scipy<1.11' requests absl-py --user

2.3 获取源码

从atomgit仓库克隆代码:

git clone https://atomgit.com/cann/ascend-boost-comm.git
cd ascend-boost-comm

仓库根目录下的version.info文件记录了版本信息,当前版本为8.5.0,要求运行时包版本不低于8.2,工具包版本也不低于8.2。

三、工程目录结构解析

拿到源码后,先花几分钟理解目录结构,这有助于后续开发时不迷路。

最核心的目录是src,里面包含所有公共组件的实现代码。src/include/mki下是对外暴露的头文件,定义了LaunchParam、KernelInfo、Types等核心数据结构和接口。src/schedule下是调度层的实现,包含operation_base(算子基类)、kernel_base(内核基类)以及通用的tensor、types等类型定义。src/mki_loader负责运行时动态加载编译好的算子二进制文件(bin文件),通过op_register宏完成算子的自动注册。

example目录是开发者接触最多的地方,里面有两个完整的算子示例:addcustom(两个张量逐元素相加)和unpad(去除张量padding)。每个算子示例都遵循相同的开发规范,后面会详细拆解addcustom的完整实现。

cmake目录存放编译配置,scripts目录包含build.sh编译脚本。tests目录下是单元测试代码,覆盖了tensor操作、状态码、平台信息、文件系统等基础组件的测试。

四、编译流程详解

ascend-boost-comm使用scripts/build.sh作为统一的编译入口。这个脚本封装了cmake的调用逻辑,支持多个编译目标。

最常用的编译目标是testframework,它会编译出测试框架的运行环境:

bash scripts/build.sh testframework

如果使用GCC 12及以上版本,加上–no_werror:

bash scripts/build.sh testframework --no_werror

编译完成后,产物会输出到output目录。需要执行set_env.sh脚本设置环境变量,后续的测试和运行都依赖这个环境配置:

source output/mki/set_env.sh

如果要编译example中的示例算子,需要在编译testframework之后再编译example:

bash scripts/build.sh testframework
bash scripts/build.sh example

这里有一个设计上的考量值得说明:为什么example的编译依赖testframework?因为example中的算子测试代码需要用到testframework提供的测试基础设施,包括op_test.py这个Python测试基类。它封装了算子执行的通用逻辑(张量构造、设备同步、结果对比等),让开发者只需关注golden函数的实现即可。

完整的编译目标可以通过help参数查看:

bash scripts/build.sh help

五、手把手开发一个自定义算子

下面以example中的addcustom算子为例,完整走一遍从零到运行的开发流程。这个算子的功能很简单:接收两个Float16类型的输入张量,在指定维度上逐元素相加,输出一个同形状的结果张量。

5.1 目录结构与文件规划

按照ascend-boost-comm的规范,一个算子需要创建以下文件结构:

example/ops/addcustom/
├── op_kernel/
│   └── addcustom.cpp          // Device侧核函数实现
├── tiling/
│   ├── tiling_data.h          // Tiling数据结构定义
│   ├── addcustom_tiling.h     // Tiling函数声明
│   └── addcustom_tiling.cpp   // Tiling函数实现
├── CMakeLists.txt             // 编译配置
├── addcustom_kernel.cpp       // Host侧Kernel类(校验+初始化)
└── addcustom_operation.cpp    // Operation类(形状推导+内核选择)

同时在example/include/atbops/params/下需要新增addcustom.h来定义算子的参数结构体。

5.2 参数结构体定义

每个算子都需要一个参数结构体,用于在Host和Device之间传递算子特定的配置信息。对于addcustom这个简单算子,参数结构体目前是空的,因为不需要额外的超参数:

#ifndef ATBOPS_PARAMS_ADDCUSTOM_H
#define ATBOPS_PARAMS_ADDCUSTOM_H

#include <cstdint>
#include <string>
#include <mki/utils/SVector/SVector.h>

namespace Mki {
namespace OpParam {
struct Addcustom {
    bool operator==(const Addcustom &other) const
    {
        (void)other;
        return true;
    }
};
} // namespace OpParam
} // namespace Mki

#endif

然后在params.h中include这个头文件,使其参与到全局的参数类型系统中。这个设计模式在整个工程中保持一致:每个算子一个参数头文件,统一在params.h中汇总。

为什么参数结构体即使为空也要定义?因为在ascend-boost-comm的调度体系中,LaunchParam通过Any类型存储算子参数,运行时通过typeid检查参数类型是否匹配。有了这个结构体,框架就能在运行时确认"这个LaunchParam确实是给Addcustom算子用的",避免参数错配的bug。

5.3 Tiling数据结构

Tiling是昇腾算子开发中的核心概念。昇腾NPU的AI Core数量有限(通常几十个),当数据量超过单个Core能处理的范围时,就需要把数据切分成多个tile,分配给不同Core并行处理。Tiling数据结构就是用来在Host和Device之间传递切分信息的。

#ifndef ASCEND_OPS_ADDCUSTOM_TILING_DATA
#define ASCEND_OPS_ADDCUSTOM_TILING_DATA

#include <cstdint>

namespace Mki {
struct AddcustomTilingData {
    uint32_t totalLength;  // 总数据长度
    uint32_t tileNum;      // Tiling块数
};
}
#endif

这里定义了两个关键字段:totalLength告诉Device侧数据的总量,tileNum告诉每个Core需要处理多少个tile。这个结构体的二进制表示会通过GM_ADDR(全局内存地址)传递给Device侧的核函数。

5.4 Host侧Tiling实现

Tiling函数运行在Host侧,负责计算数据切分策略:

#include "addcustom_tiling.h"
#include <mki/utils/assert/assert.h>
#include <mki/utils/log/log.h>
#include <mki/utils/platform/platform_info.h>
#include <mki/utils/SVector/SVector.h>
#include "atbops/params/addcustom.h"
#include "tiling_data.h"

constexpr uint32_t MIN_BLOCK_LENGTH = 32;

namespace Mki {
Status AddcustomTiling(const LaunchParam &launchParam, KernelInfo &kernelInfo)
{
    AddcustomTilingData *tilingDataPointer =
        reinterpret_cast<AddcustomTilingData *>(kernelInfo.GetTilingHostAddr());
    MKI_CHECK(tilingDataPointer != nullptr, "tilingDataPtr should not be empty",
              return Status::FailStatus(ERROR_INVALID_VALUE, "tilingDataPtr should not be empty"));

    if (launchParam.GetParam().Type() != typeid(OpParam::Addcustom)) {
        return Status::FailStatus(
            ERROR_ATTR_INVALID_TYPE,
            "Failed to check addcustom param, type of specificParam is not equals to OpParam::Addcustom");
    }

    // 获取输入张量的维度
    const uint32_t totalLength = launchParam.GetInTensor(0).desc.dims.at(0);
    MKI_LOG(INFO) << "Total length is " << totalLength;

    // 获取可用计算核心数
    uint32_t coreNum = PlatformInfo::Instance().GetCoreNum(CoreType::CORE_TYPE_VECTOR);
    MKI_LOG(INFO) << "Core number is " << coreNum;

    // 设置分块维度数
    uint32_t blockDims = 2;
    tilingDataPointer->tileNum = blockDims;
    tilingDataPointer->totalLength = totalLength;

    MKI_LOG(INFO) << "BlockDims is " << blockDims;
    MKI_LOG(INFO) << "Total length is " << tilingDataPointer->totalLength;
    MKI_LOG(INFO) << "Tile number is " << tilingDataPointer->tileNum;

    kernelInfo.SetBlockDim(blockDims);
    return Status::OkStatus();
}
} // namespace Mki

这段代码的逻辑值得仔细看。代码开头通过GetTilingHostAddr获取一块Host侧内存,这块内存最终会被拷贝到Device侧。紧接着进行参数类型检查——这层检查在框架层面起到了"类型安全"的作用,确保调用方传入了正确的参数类型。之后从LaunchParam中提取输入张量的第一个维度作为数据总长度,通过PlatformInfo查询当前硬件可用的Vector Core数量。末尾计算分块策略并写入tiling数据。

注意代码中有一行被注释掉的分块计算逻辑:

// uint32_t blockDims = std::min<uint32_t>((totalLength + MIN_BLOCK_LENGTH - 1) / MIN_BLOCK_LENGTH, coreNum);

这个注释保留了最优分块策略的思路:每个tile不小于32个元素(这是昇腾NPU向量计算单元的基本对齐要求),同时不超过可用Core数量。当前示例为了简洁直接硬编码了blockDims为2,但在生产环境中应该使用注释中的自适应计算逻辑。

5.5 Device侧核函数实现

核函数运行在昇腾NPU的Device侧,使用Ascend C编程模型。它通过Global Memory(GM)和Unified Buffer(UB)之间的数据搬运,配合向量计算单元完成实际运算:

#include "kernel_operator.h"
#include "ops/addcustom/tiling/tiling_data.h"

static constexpr uint32_t BUFFER_NUM = 1;
static constexpr uint32_t MAX_UB_SIZE = 188 * 1024; // double buffer, 每块94KB共188KB

class Addcustom {
public:
    __aicore__ inline Addcustom() {}
    __aicore__ inline void Init(GM_ADDR x, GM_ADDR y, GM_ADDR z, uint32_t totalLength, uint32_t tileNum)
    {
        this->blockLength = totalLength / AscendC::GetBlockNum();
        this->tileNum = tileNum;
        this->tileLength = this->blockLength / tileNum / BUFFER_NUM;
        xGm.SetGlobalBuffer((__gm__ half *)x + this->blockLength * AscendC::GetBlockIdx(), this->blockLength);
        yGm.SetGlobalBuffer((__gm__ half *)y + this->blockLength * AscendC::GetBlockIdx(), this->blockLength);
        zGm.SetGlobalBuffer((__gm__ half *)z + this->blockLength * AscendC::GetBlockIdx(), this->blockLength);
        pipe.InitBuffer(inQueueX, BUFFER_NUM, this->tileLength * sizeof(half));
        pipe.InitBuffer(inQueueY, BUFFER_NUM, this->tileLength * sizeof(half));
        pipe.InitBuffer(outQueueZ, BUFFER_NUM, this->tileLength * sizeof(half));
    }
    __aicore__ inline void Process()
    {
        int32_t loopCount = this->tileNum * BUFFER_NUM;
        for (int32_t i = 0; i < loopCount; i++) {
            CopyIn(i);
            Compute(i);
            CopyOut(i);
        }
    }

private:
    __aicore__ inline void CopyIn(int32_t progress)
    {
        AscendC::LocalTensor<half> xLocal = inQueueX.AllocTensor<half>();
        AscendC::LocalTensor<half> yLocal = inQueueY.AllocTensor<half>();
        AscendC::DataCopy(xLocal, xGm[progress * this->tileLength], this->tileLength);
        AscendC::DataCopy(yLocal, yGm[progress * this->tileLength], this->tileLength);
        inQueueX.EnQue(xLocal);
        inQueueY.EnQue(yLocal);
    }
    __aicore__ inline void Compute(int32_t progress)
    {
        AscendC::LocalTensor<half> xLocal = inQueueX.DeQue<half>();
        AscendC::LocalTensor<half> yLocal = inQueueY.DeQue<half>();
        AscendC::LocalTensor<half> zLocal = outQueueZ.AllocTensor<half>();
        AscendC::Add(zLocal, xLocal, yLocal, this->tileLength);
        outQueueZ.EnQue<half>(zLocal);
        inQueueX.FreeTensor(xLocal);
        inQueueY.FreeTensor(yLocal);
    }
    __aicore__ inline void CopyOut(int32_t progress)
    {
        AscendC::LocalTensor<half> zLocal = outQueueZ.DeQue<half>();
        AscendC::DataCopy(zGm[progress * this->tileLength], zLocal, this->tileLength);
        outQueueZ.FreeTensor(zLocal);
    }

private:
    AscendC::TPipe pipe;
    AscendC::TQue<AscendC::QuePosition::VECIN, BUFFER_NUM> inQueueX, inQueueY;
    AscendC::TQue<AscendC::QuePosition::VECOUT, BUFFER_NUM> outQueueZ;
    AscendC::GlobalTensor<half> xGm;
    AscendC::GlobalTensor<half> yGm;
    AscendC::GlobalTensor<half> zGm;
    uint32_t blockLength;
    uint32_t tileNum;
    uint32_t tileLength;
};

inline __aicore__ void InitTilingData(const __gm__ uint8_t *p_tilingdata, Mki::AddcustomTilingData *tilingdata)
{
    tilingdata->totalLength = ((const __gm__ uint32_t *)p_tilingdata)[0];
    tilingdata->tileNum = ((const __gm__ uint32_t *)p_tilingdata)[1];
}

extern "C" __global__ __aicore__ void addcustom(GM_ADDR x, GM_ADDR y, GM_ADDR z, GM_ADDR tiling)
{
    Addcustom op;
    Mki::AddcustomTilingData tdata;
    InitTilingData(tiling, &tdata);
    op.Init(x, y, z, tdata.totalLength, tdata.tileNum);
    op.Process();
}

这里有几个昇腾算子编程的关键设计模式需要理解。

第一,数据搬运和计算是分离的。CopyIn负责从Global Memory搬数据到Unified Buffer(即Local Tensor),Compute在Local Tensor上做加法,CopyOut把结果写回Global Memory。为什么要这样设计?因为Global Memory的带宽是有限的,而Unified Buffer虽然容量小(每块94KB)但访问速度极快。通过分块搬运、逐块计算,最大化利用高速存储的带宽优势。

第二,TPipe和TQue构成了一个流水线机制。虽然当前示例的BUFFER_NUM为1(单缓冲),但定义时就预留了double buffer的能力。把BUFFER_NUM改为2后,CopyIn可以在Compute处理当前tile的同时搬运下一个tile的数据,隐藏数据搬运延迟。这就是MAX_UB_SIZE被设为188KB(而非94KB)的原因——预留了双缓冲的空间。

第三,GetBlockIdx和GetBlockNum用于多Core并行。每个Core通过GetBlockIdx获取自己的编号,从而从Global Tensor的正确偏移位置读取数据。昇腾NPU的多个AI Core可以同时执行同一个核函数的不同实例,每个实例处理不同的数据分片,这就是昇腾并行计算的基本模型。

5.6 Kernel类与Operation类

Host侧还需要两个关键类。Kernel类负责运行时的校验和初始化:

class AddcustomKernel : public KernelBase {
public:
    explicit AddcustomKernel(const std::string &kernelName, const BinHandle *handle) noexcept
        : KernelBase(kernelName, handle) {}

    bool CanSupport(const LaunchParam &launchParam) const override
    {
        MKI_CHECK(launchParam.GetParam().Type() == typeid(OpParam::Addcustom),
                     "Addcustom valid: param type invalid", return false);
        return true;
    }

    uint64_t GetTilingSize(const LaunchParam &launchParam) const override
    {
        (void)launchParam;
        return sizeof(AddcustomTilingData);
    }

    Status InitImpl(const LaunchParam &launchParam) override
    {
        return AddcustomTiling(launchParam, kernelInfo_);
    }
};

REG_KERNEL_BASE(AddcustomKernel);

CanSupport用于判断当前Kernel是否能处理给定的LaunchParam,GetTilingSize告诉框架需要分配多大的Tiling内存,InitImpl调用前面实现的Tiling函数完成切分计算。末尾的REG_KERNEL_BASE宏将这个Kernel注册到框架的全局注册表中。

Operation类位于更高层,负责形状推导和内核选择:

class AddcustomOperation : public OperationBase {
public:
    explicit AddcustomOperation(const std::string &opName) noexcept : OperationBase(opName) {}

    Kernel *GetBestKernel(const LaunchParam &launchParam) const override
    {
        MKI_CHECK(IsConsistent(launchParam), "Fail to check consistent", return nullptr);
        MKI_CHECK(launchParam.GetParam().Type() == typeid(OpParam::Addcustom),
                     "OpParam invalid", return nullptr);
        auto dtype = launchParam.GetInTensor(0).desc.dtype;
        MKI_CHECK(dtype == TENSOR_DTYPE_FLOAT16, "Only Float16 supported", return nullptr);
        return GetKernelByName("AddcustomKernel");
    }

    int64_t GetInputNum(const Any &specificParam) const override { (void)specificParam; return 2; }
    int64_t GetOutputNum(const Any &specificParam) const override { (void)specificParam; return 1; }

protected:
    Status InferShapeImpl(const LaunchParam &launchParam, SVector<Tensor> &outTensors) const override
    {
        MKI_CHECK(launchParam.GetParam().Type() == typeid(OpParam::Addcustom),
                     "no match param type",
                     return Status::FailStatus(ERROR_INFERSHAPE_ERROR, "OpParam invalid"));
        outTensors[0].desc = launchParam.GetInTensor(0).desc;
        return Status::OkStatus();
    }
};

REG_OPERATION(AddcustomOperation);

GetBestKernel是选择逻辑的核心——它先检查输入输出一致性、参数类型,再检查数据类型是否为Float16,随后通过名字查找并返回对应的Kernel实例。这种名字匹配机制使得一个Operation可以关联多个Kernel实现(比如一个高性能版本和一个高精度版本),运行时根据输入条件选择最优的Kernel。

InferShapeImpl负责输出张量的形状推导。对于element-wise操作,输出形状等于输入形状,所以直接复制输入张量的描述信息。

5.7 Python测试用例

下一步是编写测试用例。ascend-boost-comm的测试框架基于unittest,提供了op_test.OpTest基类:

import os
import unittest
import numpy as np
import torch
import sys

sys.path.append(f"{os.environ['MKI_HOME_PATH']}/tests/pythontest")
import op_test

OP_NAME = "AddcustomOperation"
OP_PARAM0 = {"addcustomDim": 0}

class TestAddcustom(op_test.OpTest):
    def golden_calc(self, in_tensors):
        a = in_tensors[0]
        b = in_tensors[1]
        return [a + b]

    def golden_compare(self, out_tensors, golden_out_tensors):
        return torch.allclose(out_tensors[0], golden_out_tensors[0], rtol=0.001, atol=0.001)

    def test_2d_half(self):
        shape = (2 * 32,)
        a = torch.randn(shape).to(torch.float16)
        b = torch.randn(shape).to(torch.float16)
        self.set_param(OP_NAME, OP_PARAM0)
        self.execute([a, b], [torch.ones(shape).to(torch.float16)])

if __name__ == '__main__':
    unittest.main()

开发者只需要实现两个方法:golden_calc计算期望结果(这里直接用torch的加法作为参考值),golden_compare对比实际输出和期望结果的误差。execute方法由OpTest基类提供,它负责构造LaunchParam、调用Operation的InferShape和GetBestKernel、执行Kernel并把Device侧的结果同步回Host。

六、两种典型使用场景

ascend-boost-comm在实际工程中有两种典型的集成方式。

场景一:与加速库联合编译

这是最主流的用法。Ascend Transformer Boost(ATB)等加速库依赖ascend-boost-comm提供的算子调度基础设施,因此需要先编译ascend-boost-comm,再把产物拷贝到加速库的3rdparty目录下:

cd ascend-boost-comm
bash scripts/build.sh testframework
cp -r output/mki ../ascend-transformer-boost/3rdparty/

然后编译加速库:

cd ascend-transformer-boost/
source scripts/set_env.sh
bash scripts/build.sh testframework
source output/atb/set_env.sh

这种模式下,加速库通过命名空间参数与ascend-boost-comm编译产物对接。命名空间的作用是隔离不同加速库的算子符号,防止链接冲突。

场景二:单算子独立开发与测试

对于只想验证单个算子正确性的开发者,可以直接用example目录的编译流程:

cd ascend-boost-comm
bash scripts/build.sh testframework
bash scripts/build.sh example
source output/mki/set_env.sh
python example/tests/pythontest/optest/test_addcustom.py

这个流程更轻量,适合算子开发的快速迭代阶段。当算子验证通过后,再集成到完整的加速库工程中。

七、效率对比与性能考量

7.1 统一接口的复用效率

没有ascend-boost-comm时,如果ATB和信号加速库都需要一个add算子,两个团队需要分别实现各自的版本,包括各自的参数传递协议、内存管理、内核调度逻辑。保守估计,每个算子的框架胶水代码在500到1000行左右,如果50个算子在3个加速库中都需要使用,意味着要写75000到150000行重复代码。

有了ascend-boost-comm后,算子只需要实现一次。框架胶水代码被公共组件吸收,每个算子只需关注核心计算逻辑和Tiling策略。以addcustom为例,参数结构体不到20行,Tiling实现不到40行,核函数实现不到80行,加上Kernel和Operation类大约150行,总共不到300行代码就完成了一个可在所有加速库中复用的算子。这就是M乘N复用带来的代码效率提升。

7.2 编译效率

ascend-boost-comm的编译采用了增量构建设计。testframework、example、release是独立的编译目标,修改example中的算子代码不需要重新编译框架本身。这在大规模算子开发中尤为重要——如果每次改一行算子代码都要重新编译整个框架,开发效率会大幅下降。

7.3 运行时调度效率

在运行时层面,ascend-boost-comm通过op_register宏实现了静态注册,避免了运行时的字符串查找开销。GetBestKernel通过名字精确匹配Kernel,相比一些基于配置文件的动态调度方案,减少了中间环节。Tiling计算在Host侧一次性完成,结果通过内存拷贝传递给Device侧,避免了Host-Device之间反复通信的开销。

7.4 数据搬运效率

从核函数的实现可以直观看到,ascend-boost-comm的设计鼓励开发者关注数据搬运的效率。CopyIn、Compute、CopyOut的三阶段模型让开发者明确知道每一字节数据的移动路径。通过TQue和TPipe的流水线机制,数据搬运和计算可以重叠执行。在MAX_UB_SIZE为188KB的配置下,双缓冲模式可以隐藏约一半的数据搬运延迟。

以一个典型的场景来量化:假设要对一个长度为1048576(1M个float16元素,即2MB数据)的向量做加法运算,UB单次搬运94KB,需要约22次搬运。如果使用单缓冲,总时间为22次搬运时间加上22次计算时间。使用双缓冲后,大部分搬运时间和计算时间可以重叠,理论上可以节省接近一半的数据搬运等待时间。


小结

ascend-boost-comm作为昇腾计算软件栈中的基础组件,其价值不仅在于提供了统一的算子调用接口,更在于建立了一套标准化的算子开发范式。从参数结构体定义到Tiling计算,从Host侧调度到Device侧核函数实现,从Operation形状推导到Kernel选择机制,每个环节都有明确的规范和对应的基类支持。


仓库地址:http://atomgit.com/cann/ascend-boost-comm

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐