一个Token的昇腾之旅——从模型输入到硬件执行的完整链路

前言

当你在昇腾设备上运行大语言模型,输入「昇腾CANN生态很强大」这8个字符时,这些字符会先被拆成多个Token(每个Token对应一个语义单元,比如「昇」「腾」「CANN」等),每个Token就像等待运输的快递包裹,即将开启一段从软件层到硬件层的完整旅程。这段旅程跨越了昇腾异构计算架构(CANN)的多个层级,每一步都有专门的「工作人员」负责,最终在硬件中完成计算,再把结果原路送回你面前。


Step 1:AscendCL------快递打包员,给包裹贴统一面单

当你把输入文本发给模型时,第一步要做的就是把这些零散的Token(快递包裹)整理成昇腾硬件能识别的标准格式,这一步由统一编程接口AscendCL完成,它就像快递打包员,负责把用户的物品(输入数据)装进标准的快递盒,贴上统一的面单,方便后续所有环节的处理。

AscendCL的核心作用是提供统一的编程接口,屏蔽不同模型框架(TensorFlow、PyTorch等)和不同硬件型号的差异,让上层应用不用关心底层细节,只要调用AscendCL的接口就能完成数据的准备和模型的加载。对于我们的Token包裹来说,AscendCL要做的事情就是:把主机内存中的输入数据(Token对应的embedding向量)拷贝到昇腾设备的内存中,并封装成标准的aclmdlDataset数据结构,相当于给每个快递包裹贴上统一的面单,注明收件人(目标硬件)、物品类型(数据类型)、重量(数据大小)等信息。

下面是一段AscendCL准备输入数据的代码示例(C++接口),每一行都对应打包员的具体操作:

cpp 复制代码
// 1. 包含AscendCL头文件,引入所有接口定义
#include "acl/acl.h"

// 2. 初始化AscendCL资源,相当于打包员上岗,准备工具
aclError ret = aclInit(nullptr);
if (ret != ACL_SUCCESS) {
    printf("Init AscendCL failed\n");
    return -1;
}

// 3. 创建输入数据集对象,相当于准备一个快递托盘,用来放所有包裹
aclmdlDataset* inputDataset = aclmdlCreateDataset();
if (inputDataset == nullptr) {
    printf("Create input dataset failed\n");
    return -1;
}

// 4. 创建单个Tensor对象,对应一个Token包裹,设置数据地址、大小、数据类型
aclTensor* inputTensor = aclCreateTensor(
    {1, 128},                     // Tensor形状:1个Token,embedding长度128
    ACL_FLOAT16,                  // 数据类型:半精度浮点,适配昇腾硬件
    devInputPtr,                  // 设备端数据地址
    128 * sizeof(aclFloat16)      // 数据大小:128个半精度浮点数
);
if (inputTensor == nullptr) {
    printf("Create input tensor failed\n");
    return -1;
}

// 5. 把单个Tensor加入输入数据集,相当于把包裹放到托盘上
ret = aclmdlAddDatasetTensor(inputDataset, inputTensor);
if (ret != ACL_SUCCESS) {
    printf("Add tensor to dataset failed\n");
    return -1;
}

// 6. 把主机内存中的输入数据拷贝到设备内存,相当于把包裹里的物品装进标准快递盒
ret = aclrtMemcpy(
    devInputPtr,                  // 设备端目标地址
    128 * sizeof(aclFloat16),     // 拷贝大小
    hostInputPtr,                 // 主机端源地址
    128 * sizeof(aclFloat16),     // 拷贝大小
    ACL_MEMCPY_HOST_TO_DEVICE     // 拷贝方向:主机到设备
);
if (ret != ACL_SUCCESS) {
    printf("Memcpy host to device failed\n");
    return -1;
}

逐行解释:

  • 第 1 行:引入AscendCL的所有接口定义,打包员拿到工作手册,明确操作规范。
  • 第 2 行:初始化AscendCL运行环境,检查硬件资源是否可用,打包员上岗前核验工具完整性。
  • 第 3 行:创建输入数据集对象,用来存放所有输入Token,准备统一的快递托盘,所有包裹都要放在上面流转。
  • 第 4 行:创建单个Tensor对象,对应一个Token的数据,设置形状、数据类型、地址和大小,给每个包裹贴标签。
  • 第 5 行:把Tensor加入输入数据集,把包裹放到托盘上,完成打包前的归集。
  • 第 6 行:把主机内存的输入数据拷贝到设备内存,因为昇腾硬件只能访问自身设备内存,相当于把包裹物品装进标准快递盒。

AscendCL的价值正是这种统一封装能力:不同模型框架的输出格式、数据存放位置差异极大,AscendCL作为统一编程接口屏蔽了所有上层差异,让后续环节只需处理标准格式的数据------相当于快递打包员不管用户寄什么物品,都用标准盒封装、贴统一面单,大幅提升整个物流系统的流转效率。


Step 2:GE 图引擎------零散包裹拼整车,优化运输效率

当AscendCL把所有Token包裹打包成标准的aclmdlDataset之后,这些包裹并不会直接被送走,而是要先经过GE(图引擎)的处理,GE就像物流中心的拼车调度员,负责把零散的包裹(多个小算子)拼成一整车(优化后的模型图),减少运输次数,提升效率。

GE图引擎的核心作用是做图编译优化,它接收AscendCL输出的模型图(由多个算子节点和边组成),然后执行一系列优化操作:算子融合(把多个相邻小算子合并成一个大算子,减少Kernel启动次数)、内存复用(不同算子的内存块共享,减少内存分配开销)、常量折叠(提前计算模型中的常量结果,避免重复计算)。这些优化就像把零散快递包裹拼成一整车,减少运输频次、降低物流成本。

cpp 复制代码
// 1. 包含GE图引擎头文件,引入所有接口定义
#include "ge/ge_api.h"

// 2. 创建GEGraph对象,相当于准备一辆空的货车,用来装所有包裹
ge::GEGraph graph("llm_inference_model");

// 3. 添加算子节点到图中,相当于把零散包裹放到货车上
ge::Operator matmulOp("matmul_0", "MatMulV2");
matmulOp.SetInput("x", inputTensor);
matmulOp.SetInput("w", weightTensor);
matmulOp.SetOutput("y", outputTensor);
graph.AddOp(matmulOp);

ge::Operator reluOp("relu_0", "ReLU");
reluOp.SetInput("x", outputTensor);
reluOp.SetOutput("y", finalOutputTensor);
graph.AddOp(reluOp);

// 4. 设置图优化选项,相当于调度员规划拼车策略
std::map<std::string, std::string> options;
options["ge.exec.enableFusion"] = "1";           // 开启算子融合
options["ge.exec.enableMemoryReuse"] = "1";      // 开启内存复用
options["ge.exec.fusionSwitch"] = "all";         // 开启全量融合策略
ge::GraphManager::GetInstance().SetOptions(options);

// 5. 编译图,生成优化后的离线模型,相当于把包裹装车、固定位置、规划路线
ge::ModelBufferData modelBuffer;
ge::GraphManager::GetInstance().CompileGraph(graph, modelBuffer);

// 6. 保存离线模型到文件,相当于把装满包裹的货车停到待发区
ge::GraphManager::GetInstance().SaveModel(modelBuffer, "/path/to/optimized_model.om");

逐行解释:

  • 第 2 行:创建GEGraph对象,对应整个模型的计算图,准备一辆空的货车。
  • 第 3 行:添加MatMul和ReLU两个算子节点到计算图中,设置输入输出依赖关系,把零散包裹放到货车上,明确运输顺序。
  • 第 4 行:设置图优化选项,开启算子融合和内存复用,调度员规划拼车策略。
  • 第 5 行:编译计算图,GE根据优化策略对图做等价变换,把MatMul+ReLU融合成一个算子。
  • 第 6 行:保存优化后的离线模型到文件,把装满包裹的货车停到待发区,等待物流调度中心分配路线。

GE的图优化价值非常明确:原始模型图中存在大量小算子,每个算子都需要单独启动Kernel、单独搬运数据,开销极高。GE的算子融合可以减少30%以上的Kernel启动次数,内存复用可以减少20%以上的内存占用------相当于把零散包裹拼成整车,只启动一次货车、只搬运一次,效率提升显著。


Step 3:Runtime------物流调度中心,分配最优路线

当GE把优化后的离线模型准备好之后,接下来要做的就是把这个模型加载到昇腾设备上、分配计算资源,这一步由Runtime完成,它就像物流调度中心,负责根据当前硬件资源情况,给每个包裹分配最优路线,保证运输效率。

Runtime的核心作用是负责昇腾设备的资源管理、内存管理、任务调度。对于我们的Token包裹来说,Runtime要做的事情就是:把优化后的离线模型加载到昇腾设备内存中,分配空闲的AICore计算资源,把输入数据拷贝到设备内存,然后提交任务到执行队列,等待计算完成。

cpp 复制代码
// 1. 包含Runtime头文件,引入所有接口定义
#include "runtime/rt.h"

// 2. 分配设备内存,存储输入数据和模型数据,相当于调度中心准备高带宽仓库
void* devInputPtr = nullptr;
rtError_t ret = rtMalloc(
    &devInputPtr,                  // 设备内存地址指针
    128 * sizeof(aclFloat16),     // 分配大小:1个Token的embedding
    RT_MEMORY_HBM                  // 内存类型:高带宽内存
);
if (ret != RT_ERROR_NONE) {
    printf("Malloc device memory failed\n");
    return -1;
}

// 3. 拷贝输入数据从主机内存到设备内存
ret = rtMemcpy(
    devInputPtr,                  // 设备端目标地址
    128 * sizeof(aclFloat16),     // 拷贝大小
    hostInputPtr,                 // 主机端源地址
    128 * sizeof(aclFloat16),    // 拷贝大小
    RT_MEMCPY_HOST_TO_DEVICE      // 拷贝方向:主机到设备
);
if (ret != RT_ERROR_NONE) {
    printf("Memcpy host to device failed\n");
    return -1;
}

// 4. 加载优化后的离线模型到设备内存
uint32_t modelId = 0;
ret = rtModelLoad(
    "/path/to/optimized_model.om", // 离线模型路径
    &modelId                       // 输出的模型ID
);
if (ret != RT_ERROR_NONE) {
    printf("Load model failed\n");
    return -1;
}

// 5. 创建任务描述符,设置算子输入输出
rtTaskDesc taskDesc;
memset(&taskDesc, 0, sizeof(rtTaskDesc));
taskDesc.modelId = modelId;       // 绑定的模型ID
taskDesc.inputData = devInputPtr; // 输入数据地址
taskDesc.outputData = devOutputPtr; // 输出数据地址

// 6. 提交任务到执行队列,等待执行完成
ret = rtTaskSubmit(&taskDesc, sizeof(taskDesc), nullptr, nullptr);
if (ret != RT_ERROR_NONE) {
    printf("Submit task failed\n");
    return -1;
}
ret = rtTaskWait(nullptr);
if (ret != RT_ERROR_NONE) {
    printf("Wait task failed\n");
    return -1;
}

逐行解释:

  • 第 2 行:分配设备高带宽内存(HBM),HBM的带宽是普通DDR的10倍以上,准备高带宽仓库提升包裹装卸效率。
  • 第 3 行:把主机内存的输入数据拷贝到设备HBM,把包裹从集散中心(主机内存)运到调度中心仓库(设备HBM)。
  • 第 4 行:加载优化后的离线模型到设备内存,把装满包裹的货车运到调度中心停车场。
  • 第 5 行:创建任务描述符,设置模型ID、输入输出地址和大小,调度中心给货车分配路线。
  • 第 6 行:提交任务到执行队列,Runtime自动把任务分配到空闲的AICore上执行,调度中心给货车发出发指令。

Runtime的核心价值是统一资源调度:昇腾设备包含多个AICore(比如Ascend 910包含32个AICore),如果让上层应用自行分配资源,极易出现资源冲突。Runtime作为统一调度中心,会动态感知硬件资源状态,避免冲突、提升资源利用率------相当于物流调度中心统一分配路线,避免堵车,保证所有货车按时到达。


Step 4:ops-nn 算子------卡车司机,完成实际运输任务

当Runtime把任务分配到AICore之后,接下来要做的就是真正执行计算,这一步由ops-nn算子库完成,它就像卡车司机,负责把货物(数据)从出发地运到目的地,完成实际的计算任务。

ops-nn是昇腾官方算子库,提供了所有常用神经网络算子的实现,比如MatMul、ReLU、Softmax等,这些算子都是用Ascend C编程语言编写的。Ascend C是专门为昇腾达芬奇架构设计的算子编程语言,可以充分发挥硬件的计算能力。

cpp 复制代码
// 1. 包含Ascend C算子头文件,引入所有算子接口定义
#include "kernel_operator.h"

// 2. 定义向量加法算子类,封装算子的初始化和执行逻辑
class AddKernel {
public:
    __aicore__ inline AddKernel() {
        pipe.InitBuffer(inQueueT, 1, inputSize * sizeof(float));
        pipe.InitBuffer(outQueueT, 1, outputSize * sizeof(float));
    }

    __aicore__ inline void Init(uint32_t size) {
        this->inputSize = size;
        this->outputSize = size;
        inQueueT.Init(inQueueBuf, inputSize);
        outQueueT.Init(outQueueBuf, outputSize);
    }

    __aicore__ inline void Process() {
        auto inputLocal = inQueueT.AllocTensor<float>();
        inQueueT.PopTensor(inputLocal);
        auto outputLocal = outQueueT.AllocTensor<float>();
        // 向量加法:output[i] = input[i] + input[i+N/2]
        for (uint32_t i = 0; i < inputSize / 2; ++i) {
            outputLocal.SetValue(i, inputLocal.GetValue(i) + inputLocal.GetValue(i + inputSize / 2));
        }
        outQueueT.PushTensor(outputLocal);
    }

private:
    TPipe pipe;
    TQue<tQuePosition::VECIN, 1> inQueueT;
    TQue<tQuePosition::VECOUT, 1> outQueueT;
};

逐行解释:

  • 第 2 行 :定义向量加法算子类AddKernel,封装算子的初始化和执行逻辑。所有Ascend C算子都以类的形式组织。
  • 第 3 行 :构造函数初始化输入输出队列的缓冲区。pipe.InitBuffer分配SRAM上的内存,队列用来管理输入输出数据的流转。
  • 第 7 行Init方法设置输入输出大小,初始化输入输出队列。
  • 第 10 行Process方法是算子的核心计算逻辑,AllocTensor从队列分配SRAM上的局部张量。
  • 第 12 行inQueueT.PopTensor从输入队列取数据,数据从HBM搬到SRAM。
  • 第 13-15 行:在SRAM上执行向量加法计算,这是真正消耗计算资源的地方。
  • 第 16 行outQueueT.PushTensor把计算结果从SRAM推送到输出队列,数据写回HBM。

ops-nn算子的精妙之处在于它完全基于Ascend C编程,充分利用达芬奇架构的硬件特性:SRAM片上高速缓存、AI Core矩阵计算单元(Cube Unit)、向量化指令等。每个算子的实现都是对硬件能力的直接映射,没有抽象层、没有虚拟化开销------这就是为什么昇腾算子的执行效率极高。


Step 5:Driver------公路规则,让卡车能上达芬奇高速

当ops-nn算子在AICore上完成计算后,计算结果需要写回HBM内存,然后通过PCIe返回给主机。这个过程中,Driver(驱动层)扮演的角色就像公路规则------它定义了卡车(算子计算结果)如何驶上达芬奇高速(昇腾达芬奇架构),以及高速上的交通规则(内存访问协议、数据传输机制)。

Driver层负责管理昇腾设备和主机之间的通信,包括PCIe总线的配置与数据传输、DMA(直接内存访问)引擎的控制、HBM内存的地址空间管理等。对于Token包裹来说,Driver要做的事情就是:确保计算结果能正确地从昇腾设备(HBM)传输回主机内存,同时处理设备中断、错误恢复等底层事务。

cpp 复制代码
// 1. 包含Driver头文件,引入驱动层接口定义
#include "driver/drv.h"

// 2. 初始化驱动,检测并配置昇腾设备
drvError_t drvRet = drvInit();
if (drvRet != DRV_SUCCESS) {
    printf("Init driver failed\n");
    return -1;
}

// 3. 获取昇腾设备句柄,用于后续操作
drvDevice_t* device = nullptr;
drvRet = drvGetDevice(0, &device);
if (drvRet != DRV_SUCCESS) {
    printf("Get device failed\n");
    return -1;
}

// 4. 配置PCIe DMA传输,从设备HBM传输数据到主机内存
drvDmaDesc dmaDesc;
drvDmaDescInit(&dmaDesc, device);
drvDmaDescSetSrc(&dmaDesc, devOutputPtr);    // 源地址:设备HBM
drvDmaDescSetDst(&dmaDesc, hostOutputPtr);   // 目标地址:主机内存
drvDmaDescSetSize(&dmaDesc, 128 * sizeof(aclFloat16)); // 传输大小
drvDmaDescSetDir(&dmaDesc, DRV_DMA_DIR_DEVICE_TO_HOST); // 传输方向

// 5. 启动DMA传输,等待完成
drvRet = drvDmaSubmit(&dmaDesc);
if (drvRet != DRV_SUCCESS) {
    printf("Submit DMA failed\n");
    return -1;
}
drvRet = drvDmaWait(&dmaDesc, nullptr);
if (drvRet != DRV_SUCCESS) {
    printf("Wait DMA failed\n");
    return -1;
}

逐行解释:

  • 第 2 行:初始化驱动,检测并配置昇腾设备,相当于公路管理员检查高速入口是否正常开放。
  • 第 3 行:获取昇腾设备句柄,后续所有设备操作都需要这个句柄。
  • 第 4 行:配置PCIe DMA传输描述符,设置源地址(HBM)、目标地址(主机内存)、传输大小。DMA引擎可以在不占用CPU的情况下直接搬运数据,是高速数据传输的关键。
  • 第 5 行:提交DMA传输请求,驱动会启动PCIe总线的数据传输。
  • 第 6 行:等待DMA传输完成,确保数据安全到达主机内存。

Driver层的核心价值在于它定义了昇腾设备与主机之间的物理传输规则。无论是AscendCL、GE还是Runtime,它们的数据传输最终都要落实到Driver层的PCIe DMA操作上。没有Driver层,软件的计算结果就永远无法返回给用户------相当于高速公路没有建成,所有卡车都上不了路。


Step 6:达芬奇架构------高速公路和计算工厂

经历了上述五个步骤,Token包裹终于到达了旅程的最后一站------昇腾达芬奇架构。达芬奇架构是昇腾芯片的计算核心,它就像一条超级高速公路和一座巨型计算工厂的结合体,包含了大量的AICore(AI Core,计算核心)、缓存系统和总线互联结构。

达芬奇架构的核心组件是AICore,每个AICore包含三大计算单元:

  • Cube Unit(矩阵计算单元):专门用于大矩阵乘法,性能是普通向量单元的16倍以上,是Transformer模型计算的核心引擎。
  • Vector Unit(向量计算单元):执行逐元素操作(ReLU、Softmax等),处理Cube Unit无法处理的其他计算。
  • Scalar Unit(标量计算单元):控制流、地址计算、循环控制等,相当于工厂的管理系统。

对于Token包裹来说,达芬奇架构的AICore是最终的目的地------算子(如MatMul、LayerNorm)的计算指令被下发给AICore,在Cube Unit中完成矩阵乘法,在Vector Unit中完成归一化,在Scalar Unit中完成数据地址的计算,最终结果写回到HBM的指定位置。

复制代码
达芬奇架构的计算流水线:

主机内存 ──PCIe──> HBM高带宽内存 ──> SRAM片上缓存 ──> AICore Cube Unit
                  ▲                                        │
                  │                                        ▼
               结果写回                               计算输出
                  ▲                                        │
                  │                                        ▼
                  └────── SRAM片上缓存 ──> HBM ──PCIe──> 主机内存

整个数据流的关键在于SRAM是AICore的"速记纸":每次计算时,数据从HBM(慢速但容量大)加载到SRAM(快速但容量小),在AICore上完成计算,结果写回HBM。这个过程不断循环,直到整个模型的计算全部完成。达芬奇架构的设计哲学正是"让数据在靠近计算单元的地方快速访问",通过层层缓存减少对慢速HBM的访问次数,从而提升整体性能。


旅程终点站:Token 完成计算,原路返回

当Token经过AscendCL打包、GE图引擎拼车、Runtime调度、ops-nn算子执行、Driver公路规则,最终在达芬奇架构的AICore中完成计算后,计算结果会原路返回:

  1. 计算结果从AICore写回HBM
  2. Driver的DMA引擎通过PCIe将结果传输回主机内存
  3. Runtime通知上层任务完成
  4. AscendCL将结果封装成标准的aclmdlDataset,返回给用户

整个旅程的每一层都各司其职:AscendCL负责统一打包,GE负责图优化,Runtime负责资源调度,ops-nn负责算子执行,Driver负责物理传输,达芬奇架构负责最终的计算。这种分层设计的好处是每层都可以独立演进------比如新的算子只需要在ops-nn层实现,无需改动AscendCL或Runtime;新的硬件只需要更新Driver层,上层软件无需感知。

这种优雅的架构设计,正是昇腾CANN生态能够在国际竞争中保持技术领先的底层逻辑之一。


相关推荐
renke336411 小时前
写给前端的 CANN-torchtitan-npu:昇腾PyTorch Titan适配到底是啥?
前端·人工智能·pytorch·cann
灰灰勇闯IT2 天前
MindSpore 和 CANN 是什么关系——用一个厨房讲明白
人工智能·深度学习·算法·cann
昇腾CANN3 天前
芯模赋能,智启未来:杭电CANN启航营圆满收官,解锁AI实践
人工智能·昇腾·cann
林夕073 天前
Qt集成AI推理引擎:TensorFlow Lite与ONNX Runtime实战
人工智能·qt·neo4j
格鸰爱童话5 天前
springboot3.2使用neo4j
springboot·neo4j
昇腾CANN8 天前
5月14号直播丨多模态生成技术优化实践第二期--并行和Cache篇
人工智能·昇腾·cann
Yeats_Liao8 天前
智能感知低功耗设计:MCU上的AI异常检测与能效优化
人工智能·单片机·物联网·neo4j
wjykp8 天前
1.neo4j琐碎知识
数据库·neo4j
一个数据大开发9 天前
企业知识工程的三条路线:Neo4j 知识中台、Agent + Action 与本体原生 Runtime
大数据·python·neo4j