从Ascend C算子开发视角看CANN的“软硬协同”

从Ascend C算子开发视角看CANN的"软硬协同"

  • 写在最前面
    • 引言:当软件试图控制"流水线"
    • [一、 编程模型视角:软件定义的流水线](#一、 编程模型视角:软件定义的流水线)
      • [1.Host 侧:Tiling(切分)的智慧](#1.Host 侧:Tiling(切分)的智慧)
      • [2. Device 侧:Kernel(核函数)的执行](#2. Device 侧:Kernel(核函数)的执行)
    • [二、 物理底座:认识你的"施工队" (AI Core)](#二、 物理底座:认识你的“施工队” (AI Core))
    • [三、 存储与搬运:一场"螺蛳壳里做道场"的游戏](#三、 存储与搬运:一场“螺蛳壳里做道场”的游戏)
    • [四、 核心脉络:典型数据流与指令流](#四、 核心脉络:典型数据流与指令流)
      • [1. 典型数据流 (Data Flow)](#1. 典型数据流 (Data Flow))
      • [2. 典型指令流 (Instruction Flow)](#2. 典型指令流 (Instruction Flow))
    • [五、 Ascend C编程模型:流水线上的"艺术"](#五、 Ascend C编程模型:流水线上的“艺术”)
    • [六、 总结:Ascend C 到底在控制什么?](#六、 总结:Ascend C 到底在控制什么?)


🌈你好呀!我是 是Yu欸 🚀 感谢你的陪伴与支持~ 欢迎添加文末好友 🌌 在所有感兴趣的领域扩展知识,不定期掉落福利资讯(*^▽^*)


写在最前面

版权声明:本文为原创,遵循 CC 4.0 BY-SA 协议。转载请注明出处。

引言:当软件试图控制"流水线"

在AI算力飙升的今天,我们手里的NPU(神经网络处理器)越来越像一座精密的巨型工厂。以往,我们习惯用"黑盒"的方式去使用它------喂入数据,等待结果。但在大模型训练和极致推理优化的深水区,这种模式行不通了。

为什么我的算子跑不满算力?为什么数据搬运占用了80%的时间?

要回答这些问题,我们需要打破软件和硬件的界限。

在昇腾 CANN (Compute Architecture for Neural Networks) 的架构体系中,Ascend C 编程语言被推向了舞台中央。虽然 CANN 依然保留着经典的五层架构作为坚实的底座,但对于追求极致性能的算子开发者而言,视角已经从"调用层级"转向了更扁平、更直接的 "主机-设备(Host-Device)协同编程模型"

站在 算子开发者 的视角,看一看基于 Ascend C 的代码是如何在底层 达芬奇(Da Vinci)架构 的物理流水线上流动的。


一、 编程模型视角:软件定义的流水线

图注:Ascend C 算子逻辑架构。左侧 Host 负责切分策略(Tiling),右侧 Device (Kernel) 负责计算执行。图片来源: 昇腾社区 CANN 8.3 文档

当我们谈论"CANN的新架构体验"时,我们实际上是在谈论 Ascend C 的 SPMD(单程序多数据)编程模型 。这不是替代了CANN原有的架构图,而是将物理硬件的抽象直接映射到了代码逻辑中

核心逻辑非常简单:Host 侧决定"怎么切",Device 侧决定"怎么算"。

1.Host 侧:Tiling(切分)的智慧

在处理动态 Shape(形状可变)的场景时,AI Core 内部有限的缓存(L1/UB 通常仅几百 KB)无法一次性容纳巨大的 Tensor(几百 MB)。

  • 痛点:数据太大,"胃口"太小。
  • 解决 :Host 侧运行的 Tiling 函数,本质上是在计算**"这一刀该切多大"**。它根据当前输入数据的 Shape 和硬件的 Core 数量,动态计算出 BlockDim(核数)和 TileSize(切片大小)。
  • 注意:虽然 Tiling 是高性能算子开发的标配,但它并非强制。对于极其简单的静态小算子,你甚至可以硬编码,但在通用的高性能场景下,Tiling 是通过软件挖掘硬件并行度的关键一环。

2. Device 侧:Kernel(核函数)的执行

Kernel 函数是真正的"打工人"。一份 C++ 代码会被复制到芯片上成千上万个 AI Core 中并行执行。每个 Core 领取属于自己的任务包,只处理自己那一亩三分地的数据。

二、 物理底座:认识你的"施工队" (AI Core)

一切软件逻辑,最终都要落地到硬件执行。在昇腾处理器中,最小的计算与调度单元被称为 AI Core。Ascend C 算子(Kernel)最终就是在这里运行。

为了实现极致的能效比,AI Core 内部组建了一支分工明确、高度专业化的"施工队":

1、三大计算工种

这三类单元虽然都在 AI Core 内部,但它们的职责完全不同:

  • Cube Unit(矩阵搬砖工):

它是绝对的算力担当,专门干重活。它只做一件事:矩阵乘法。哪怕是简单的加法,它也得凑成矩阵去算。

它的特点是"吞吐量巨大",一铲子下去(一个指令周期),能算完一个 16 × 16 × 16 16\times16\times16 16×16×16 的立方体运算。

它比较挑剔,原料必须放在 L0 Buffer(L0A/L0B)里才肯干活。

图注:Cube计算单元数据访问。亮部分为Cube计算单元及其访问的存储单元,其中L0A存储左矩阵,L0B存储右矩阵,L0C存储矩阵乘的结果和中间结果。图片来源: 昇腾社区 CANN 8.3 文档

  • Vector Unit(向量精细工):

它负责精细活。Cube干不了的非矩阵运算(如激活函数ReLU、归一化LayerNorm、倒数Reciprocal)都归它管。

它的特点是"灵活",支持SIMD(单指令多数据),一次处理一条向量。

它的工作台是 Unified Buffer (UB),所有数据必须先搬到 UB 才能被处理。

图注:__Vector计算单元数据访问。Vector所有计算的源数据以及目标数据都要求存储在Unified Buffer中,Vector__指令__的首地址和操作长度有对齐要求,通常要求32B对齐,具体对齐要求参考__API__的约束描述。图片来源: 昇腾社区 CANN 8.3 文档

  • Scalar Unit(工头):

它就是AI Core里的小CPU。它不直接干重活,而是负责读代码(指令)、做逻辑判断(if/else)、算地址索引,然后给Cube和Vector派活。

图注:Scalar 指令 和数据的访问__。Scalar执行__标量__运算指令时,执行标准的ALU(Arithmetic Logic Unit)语句,ALU需要的代码段和数据段(栈空间)都来自于GM,ICache(Instruction Cache)用于缓存代码段,缓存大小与硬件规格相关,比如为16K或32K,以2K为单位加载;DCache(Data Cache)用于缓存数据段,大小也与硬件规格相关,比如为16K,以Cache Line(64Byte)为单位加载。考虑到核内访问效率最高,应尽量保证代码段和数据段被缓存在ICache和DCache,避免核外访问; 同时根据数据加载单位不同,编程时可以考虑单次加载数据大小,来提升加载效率。例如在DCache加载数据时,当数据__内存__首地址与Cache Line(64Byte)对齐时,加载效率最高。图片来源: 昇腾社区 CANN 8.3 文档

2、两种工作模式

在不同型号的昇腾芯片上,这些工种的组织方式是不一样的,这也是 CANN 文档中提到的"工作模式".

在最新的 Atlas A2 训练/推理系列 产品中,这支队伍出现了一种新的组织形式------分离模式

  • Cube Core (AIC):只有矩阵搬砖工和工头。

**图1 **分离模式示意图(N的取值以硬件平台信息获取接口获取的数值为准)

  • Vector Core (AIV):只有向量精细工和工头。

这就像把"砌墙的"和"装修的"分到了不同的车间,互不干扰,由系统按比例(如1:N)组合调用。

**图2 **耦合模式示意图


三、 存储与搬运:一场"螺蛳壳里做道场"的游戏

算力再强,数据供不上也是白搭。AI Core 内部设计了一套极其复杂的"物流系统"来确保存储和计算的平衡。

Ascend C编程最核心的难点,也是CANN架构最精妙的地方,在于内存的显式管理

1、仓库(存储单元)

Ascend C 编程中提到的 LocalTensor,实际上就对应着下面这些物理 Buffer:

  • Unified Buffer (UB)Vector 的专用案板。Ascend C 中最常用的 Buffer,几乎所有 Vector 运算都要经过它。
  • L1 Buffer中转仓库。容量较大,用于暂存从片外运进来的数据,给 Cube 备货。
  • L0 Buffer (A/B/C)Cube 的专用料斗。L0A/B 存输入矩阵,L0C 存计算结果。
  • BiasTable / Fixpipe Buffer:存放偏置项和量化参数的专用小柜子。

**图注 **存储单元。

2、为什么不能直接用全局内存?

Global Memory (GM) 就像远在郊区的"总仓库",容量大但速度慢。如果Cube计算每一次都要去GM拿数据,那99%的时间都浪费在路上了。

所以,AI Core内部建了几个"临时中转站":

  • L1 Buffer:Cube的大型中转站。
  • L0 Buffer (A/B/C):Cube的"手边料斗"。L0A/L0B放输入矩阵,L0C放结果。
  • Unified Buffer (UB) :Vector的"专用案板"。所有Vector的计算,数据必须先搬到UB里才能开始!

3、MTE与搬运通路

为了把数据在这些仓库之间挪来挪去,硬件里有一组专门的搬运工,叫 MTE (Memory Transfer Engine)

  • MTE1 :负责**"内转"** 。主要路径是 L1 -> L0
  • MTE2:负责把货物从 郊区仓库(GM) 拉到 核心中转站(L1/UB)。
  • MTE3:负责把做好的菜从 核心(UB) 运回 郊区仓库(GM)。
  • FixPipe:负责**"后处理"** 。Cube 算完的结果(L0C),经过它进行量化或格式转换后,写回 GM 或 L1。

Ascend C的本质,就是写代码指挥MTE、Cube、Vector这三拨人配合工作。


四、 核心脉络:典型数据流与指令流

理解了硬件组件,我们就能看懂数据和指令是怎么流动的。这是写好 Ascend C 代码的关键。

1. 典型数据流 (Data Flow)

Ascend C 的 DataCopy 接口,本质上就是在驱动数据走下面这两条路:

  • 路径 A:Vector 计算流(灵巧型)
plain 复制代码
graph LR
GM[Global Memory] -->|MTE2 搬入| UB
UB -->|Vector 计算| UB
UB -->|MTE3 搬出| GM
  • 解读: 数据从大仓库直接拉到 UB 案板,切完菜(计算),再直接送回大仓库。路径短,延迟低。
  • 路径 B:Cube 计算流(吞吐型)
  • 代码段
plain 复制代码
graph LR
GM -->|MTE2| L1
L1 -->|MTE1| L0A/L0B
L0A/L0B -->|Cube 计算| L0C
L0C -->|FixPipe| GM
  • 解读: 这是一个深度的流水线。数据要经过多级缓存(L1->L0),虽然路径长,但可以通过双缓冲(Double Buffering) 机制,让搬运和计算完美重叠。

2. 典型指令流 (Instruction Flow)

如果你看懂了指令流,你就明白了为什么昇腾能做到"极致并行"。

所有的指令最初都进入 ICacheScalar 单元 读取这些指令后,会像分发传单一样,把不同类型的指令发射到不同的独立队列中:

  • Vector****指令 → \rightarrow → 扔进 Vector Queue
  • **Cube **指令 → \rightarrow → 扔进 Cube Queue
  • 搬运****指令 → \rightarrow → 扔进 MTE Queue

关键点: 这些队列是并行工作的!

这意味着,当 MTE 正在搬运第 2 块数据时,Vector 可能正在计算第 1 块数据。这就是硬件级的流水线并行。

**图 **指令分类处理机制


五、 Ascend C编程模型:流水线上的"艺术"

理解了硬件,再看Ascend C的代码结构,你就会发现它完全是硬件逻辑的映射

1、SPMD:一生万物

Ascend C采用 SPMD (Single Program, Multiple Data) 模式。

你只需要写一份代码(Kernel),系统会把它复制到芯片上成千上万个AI Core里同时运行。

  • 关键点 :每个Core怎么知道自己该干哪部分活?通过 GetBlockIdx() 拿到自己的身份证号,然后去Global Memory里认领属于自己的那块数据切片。

**图 **SPMD数据并行示意图

2、核心范式:TQue 与 流水线

怎么让MTE搬运的时候,Cube不闲着?CANN设计了一套标准的流水线(Pipeline)编程范式

想象一个做菜的过程:买菜(CopyIn) -> 切菜(Compute) -> 上菜(CopyOut)。

如果顺序执行,买菜时厨师在玩手机。

Ascend C引入了 TQue (任务队列) 来实现流水线并行:

  • 阶段1 :MTE把第一波菜买回来,放进 Q_in 队列。
  • 阶段2
    • Vector从 Q_in 拿菜去切。
    • 同时,MTE转头去买第二波菜。
  • 双缓冲 (Double Buffering):为了极致效率,我们通常申请两块内存(Ping-Pong)。当Vector在处理Buffer A时,MTE正在往Buffer B里写数据。

**图 **流水线并行示意图

3、同步机制:红绿灯

因为大家是并行干活的,必须有红绿灯(同步)机制。

  • EnQue (入队):MTE喊话:"菜买回来了(Buffer已满),厨师你可以拿了!"
  • DeQue** (出队)**:厨师喊话:"菜我切完了(Buffer已空),搬运工你可以拿去装下一波菜了!"

在Ascend C代码中,这些复杂的硬件同步逻辑被封装成了简单的 EnQue / DeQue 接口,开发者只要照着模板填空,就能写出跑满流水线的代码。

plain 复制代码
// ...
namespace AscendC {
class MyKernel {
public:
    __aicore__ inline MyKernel() {}
    __aicore__ inline void Init(__gm__ uint8_t* src0Gm, __gm__ uint8_t* src1Gm, __gm__ uint8_t* dstGm)
    {
        src0Global.SetGlobalBuffer((__gm__ half*)src0Gm);
        src1Global.SetGlobalBuffer((__gm__ half*)src1Gm);
        dstGlobal.SetGlobalBuffer((__gm__ half*)dstGm);
        pipe.InitBuffer(srcQue0, 1, BLOCK_SIZE * sizeof(half));
        pipe.InitBuffer(srcQue1, 1, BLOCK_SIZE * sizeof(half));
        pipe.InitBuffer(dstQue0, 1, BLOCK_SIZE * sizeof(half));
    }

    __aicore__ inline void Process()
    {
        for (int i = 0; i < REPTIMES; i++) {
            CopyIn(i);
            Compute(i);
            CopyOut(i);
        }
    }

private:
    __aicore__ inline void CopyIn(int32_t i)
    {
        srcQue0.AllocTensor<half>(src0Local);
        srcQue1.AllocTensor<half>(src1Local);
        DataCopy(src0Local, src0Global[i*BLOCK_SIZE], BLOCK_SIZE);
        DataCopy(src1Local, src1Global[i*BLOCK_SIZE], BLOCK_SIZE);
        srcQue0.EnQue(src0Local);
        srcQue1.EnQue(src1Local);
    }
    __aicore__ inline void Compute(int32_t i)
    {
        srcQue0.DeQue<half>(src0Local);
        srcQue1.DeQue<half>(src1Local);
        dstQue0.AllocTensor<half>(dstLocal);
        Add(dstLocal, src0Local, src1Local, BLOCK_SIZE);
        dstQue0.EnQue<half>(dstLocal);
        srcQue0.FreeTensor(src0Local);
        srcQue1.FreeTensor(src1Local);
    }
    __aicore__ inline void CopyOut(int32_t i)
    {
        dstQue0.DeQue<half>(dstLocal);
        DataCopy(dstGlobal[i*BLOCK_SIZE], dstLocal, BLOCK_SIZE);
        dstQue0.FreeTensor(dstLocal);
    }

private:
    TPipe pipe;
    TQue<QuePosition::VECIN, 0> srcQue0, srcQue1;
    TQue<QuePosition::VECOUT, 0> dstQue0;
    GlobalTensor<half> src0Global, src1Global, dstGlobal;
    LocalTensor<half> src0Local;
    LocalTensor<half> src1Local;
    LocalTensor<half> dstLocal;
};
} // namespace AscendC

// ...

六、 总结:Ascend C 到底在控制什么?

回看 Ascend C 的编程范式,你会发现它所有的设计都是为了配合上述的硬件逻辑:

  1. TQue 队列 :是为了配合 Scalar 的指令发射机制,实现不同单元(MTE/Vector)之间的异步****并行
  2. PipeBarrier / WaitFlag:是因为不同队列跑得快慢不一,开发者必须在代码里手动插入"红绿灯"(同步指令),防止 Vector 还没算完,MTE 就把数据覆盖了。
  3. Tiling(切分):是因为 L1/UB 的容量有限(通常几百 KB),必须把以 GB 计的模型数据切成小块,分批塞进 AI Core 处理。

一句话总结: CANN 8.3 带来的 Ascend C,不再试图对开发者隐藏硬件细节,而是将 Cube/Vector/Scalar 的三级流水线L1/L0/UB 的多级缓存 直接暴露出来。

当你写下一行 DataCopy 时,你不是在调函数,你是在指挥 MTE 引擎发动引擎;当你写下一行 SetFlag 时,你是在控制 Scalar 单元按下同步的红灯。 这就是昇腾高性能计算的真相。

欢迎扫码预约昇腾CANN官方直播,获取更多一线解读:


参考资料

  1. Ascend C 算子开发指南 (CANN 8.3 商用版)
    1. 链接:(https://www.hiascend.com/document/detail/zh/canncommercial/83RC1/opdevg/Ascendcopdevg/atlas_ascendc_10_0008.html)
    2. 说明:包含最新的Ascend C编程范式、SPMD模型及流水线同步机制详解。
  2. 昇腾AI处理器逻辑架构详解
    1. 链接:(https://www.hiascend.com/document/detail/zh/canncommercial/83RC1/opdevg/Ascendcopdevg/atlas_ascendc_10_00027.html)
    2. 说明:官方文档关于AI Core、Cube/__Vector__单元及分离/耦合__架构的详细定义。
  3. 昇腾社区(HiAscend)
    1. 链接:https://www.hiascend.com
    2. 说明:获取最新CANN版本更新日志及开发者资源的入口。

hello,我是 是Yu欸。如果你喜欢我的文章,欢迎三连给我鼓励和支持:👍点赞 📁 关注 💬评论,我会给大家带来更多有用有趣的文章。

原文链接 👉 ,⚡️更新更及时。

欢迎大家点开下面名片,添加好友交流。

相关推荐
黎雁·泠崖2 小时前
C 语言字符串进阶:strcpy/strcat/strcmp 精讲
c语言·开发语言
赴前尘2 小时前
golang获取一个系统中没有被占用的端口
开发语言·后端·golang
嘴贱欠吻!2 小时前
JavaSE基础知识
java·开发语言
逝水如流年轻往返染尘3 小时前
Java输入输出
java·开发语言
hqwest3 小时前
码上通QT实战03--登录逻辑
开发语言·qt·登录·嵌入式实时数据库·界面设计
Alsn863 小时前
29.Java中常见加解密算法的基本实现
java·开发语言·算法
独自破碎E3 小时前
什么是Java的多态特性?
java·开发语言
superman超哥3 小时前
仓颉GC调优参数深度解析
c语言·开发语言·c++·python·仓颉
sandyznb3 小时前
go面试汇总
开发语言·面试·golang