前言
昇腾(Ascend)计算架构中的CANN(Compute Architecture for Neural Networks)是连接上层深度学习框架与底层昇腾NPU硬件的核心软件层。CANN承接了来自TensorFlow、PyTorch、MindSpore等主流框架的计算任务,将其转化为可执行在昇腾NPU上的算子指令流。在CANN的软件栈中,driver仓库是最底层的软件组件,直接管理昇腾加速卡的固件加载、设备初始化和硬件资源抽象,其代码运行在内核空间,与Linux内核子系统深度耦合。driver仓的稳定性和正确性是整个昇腾软件栈正常运转的前提条件——一旦driver层出现故障,所有上层软件都将无法与硬件正常通信。从架构分层角度看,CANN driver处于整个软件栈的最底层,向上为CCE(Cube-based Computing Engine)编译器提供统一的设备抽象接口,向下直接操作PCIe总线寄存器、DDR控制器和中断控制器等硬件资源。理解driver仓的设计原理和实现机制,是深入掌握昇腾计算架构的关键切入点。
驱动模块结构
模块划分总览
driver仓内部并非一个单一的、内聚的模块,而是由多个职责明确的子模块组成的一个分层架构体系。从功能划分的角度来看,driver仓主要包含三大核心模块:soc模块、ctrl模块和devdrv模块。这三个模块在代码层面各自独立编译,在运行时按照严格的依赖顺序依次加载,共同完成昇腾加速卡的初始化和控制工作。
soc模块(System on Chip)是driver仓中最贴近硬件的模块,负责芯片级资源的管理工作。soc模块直接与硬件寄存器打交道,管理SoC内部的时钟树、电源域、DDR内存控制器、调试总线等基础设施资源。在昇腾加速卡上电的初期阶段,只有soc模块能够正确识别和管理硬件状态,其他所有模块都依赖于soc模块提供的底层服务。soc模块的设计遵循平台无关性的原则,通过抽象层接口屏蔽不同芯片型号之间的硬件差异,使得上层模块可以在不同型号的昇腾芯片上统一运行。
ctrl模块(Control Module)位于soc模块之上,负责设备控制流的编排和管理工作。ctrl模块维护设备的状态机,处理设备上下电的时序逻辑,协调多个硬件子系统的启动顺序。ctrl模块还负责与Host端驱动进行命令交互,接收来自用户空间或内核上层的控制指令,并将其转化为对硬件的具体操作。在设备状态管理方面,ctrl模块维护了从设备未被识别到完全可用的完整状态迁移图,确保每一步操作都在正确的时机执行,避免出现时序相关的硬件故障。
devdrv模块(Device Driver Module)是driver仓对外暴露的统一设备抽象层,负责向上层软件提供标准化的设备访问接口。devdrv模块将底层的硬件细节封装在内部,向上层CCE编译器和算子运行库提供统一的内存分配、设备控制、事件同步等接口。devdrv模块的设计参考了Linux内核的设备驱动模型,遵循统一设备模型(UDM)的设计理念,使得不同类型的昇腾加速卡可以使用同一套接口进行访问。
加载顺序与依赖关系
driver仓内部三个模块的加载顺序受到严格的依赖关系约束,错误的加载顺序会导致硬件无法正常初始化甚至造成不可恢复的硬件损伤。整个加载流程严格按照soc模块、ctrl模块、devdrv模块的顺序执行,每个模块在加载之前必须确认其所有依赖模块已经成功完成加载并处于可用状态。
soc模块作为最底层的模块,没有任何对其他driver子模块的依赖,但它对内核基础设施存在依赖关系。soc模块在加载时优先完成内核内存管理子系统的hook注册,建立与Linux MM(Memory Management)子系统的通信通道。紧随后soc模块扫描并识别连接到PCIe总线上的昇腾加速卡,为每一张检测到的加速卡分配独立的软件描述符,并将硬件信息注册到内核设备模型中。soc模块加载完成后,系统已经能够在/proc或/sysfs中看到昇腾设备的存在,但此时设备尚处于不可用状态。
ctrl模块在soc模块加载完成后立即被触发加载,它依赖soc模块提供的硬件访问接口和设备发现服务。ctrl模块在加载阶段主要完成两件事:一是初始化设备状态机,将所有已发现的昇腾加速卡设置为初始(INIT)状态;二是建立与固件子系统( firmware subsystem)的通信通道,为后续的固件加载做好准备。ctrl模块在加载过程中会注册一系列字符设备节点,允许用户空间程序通过标准文件操作接口与驱动进行交互。
devdrv模块在加载阶段末尾被触发,它依赖soc模块的硬件抽象接口和ctrl模块的状态管理服务。devdrv模块在加载时完成设备抽象层的最终初始化工作,包括分配内存管理数据结构、初始化同步原语(如事件对象和信号量)、注册字符设备接口等。devdrv模块加载完成后,昇腾加速卡正式进入可用(AVAILABLE)状态,上层软件可以通过标准的设备接口发起计算任务。
以下代码段展示了driver模块初始化的核心入口函数,演示了三个模块的加载顺序关系。
static int __init ascend_driver_init(void)
{
int ret;
/* soc module must be loaded first, as it provides
* hardware access primitives for all other modules */
ret = soc_module_init();
if (ret != 0) {
pr_err("[DRIVER] soc module init failed: %d\n", ret);
return ret;
}
/* ctrl module depends on soc module's hardware interfaces */
ret = ctrl_module_init();
if (ret != 0) {
pr_err("[DRIVER] ctrl module init failed: %d\n", ret);
soc_module_exit();
return ret;
}
/* devdrv module is the top-level interface, loaded last */
ret = devdrv_module_init();
if (ret != 0) {
pr_err("[DRIVER] devdrv module init failed: %d\n", ret);
ctrl_module_exit();
soc_module_exit();
return ret;
}
pr_info("[DRIVER] all modules loaded successfully\n");
return 0;
}
设备初始化流程
PCIe枚举与设备发现
昇腾加速卡通过PCIe接口与Host服务器连接,因此设备初始化流程的起点是Linux内核的PCIe子系统对昇腾加速卡的枚举过程。当系统加电或昇腾加速卡热插拔时,PCIe总线驱动会扫描总线上的所有设备,识别出厂商ID(Vendor ID)和设备ID(Device ID)符合昇腾规范的设备,并将它们作为PCIe function注册到内核设备树中。
driver仓通过自定义的PCIe ID table来匹配昇腾加速卡。在驱动加载阶段,driver模块会注册自己的PCIe driver结构体,其中包含支持的所有昇腾设备ID列表。当PCIe子系统完成枚举并发现匹配的设备时,会回调driver中的probe函数。probe函数是整个设备发现流程中最关键的处理函数,它负责为每个检测到的昇腾加速卡分配私有数据结构、映射PCIe BAR(Base Address Register)空间、注册中断处理函数等核心工作。
BAR空间的映射是probe阶段的重要操作。昇腾加速卡在PCIe配置空间中声明了多个BAR区域,每个BAR对应加速卡上的一类硬件寄存器组。通过ioremap操作,driver将这些物理地址映射为内核虚拟地址,使得CPU可以通过内存访问指令(而不是特殊的I/O指令)来读写加速卡的寄存器。不同型号的昇腾加速卡可能具有不同数量和大小的BAR区域,driver代码需要根据设备的实际配置动态调整映射策略。
以下代码展示了PCIe probe函数中对BAR空间的典型映射处理流程。
static int ascend_pcie_probe(struct pci_dev *pdev,
const struct pci_device_id *id)
{
struct ascend_device *adev;
int bar;
adev = devm_kzalloc(&pdev->dev, sizeof(*adev), GFP_KERNEL);
if (!adev)
return -ENOMEM;
if (pci_enable_device(pdev) != 0) {
dev_err(&pdev->dev, "failed to enable PCI device\n");
return -ENODEV;
}
/* map all available BAR regions */
for (bar = 0; bar < PCI_STD_NUM_BARS; bar++) {
resource_size_t start = pci_resource_start(pdev, bar);
resource_size_t len = pci_resource_len(pdev, bar);
if (!len)
continue;
adev->bar[bar] = devm_ioremap_resource(&pdev->dev, &pdev->resource[bar]);
if (IS_ERR(adev->bar[bar])) {
dev_err(&pdev->dev, "BAR%d ioremap failed\n", bar);
return PTR_ERR(adev->bar[bar]);
}
dev_info(&pdev->dev, "BAR%d mapped at %px, size %pap\n",
bar, adev->bar[bar], &len);
}
pci_set_master(pdev);
adev->pdev = pdev;
pci_set_drvdata(pdev, adev);
return 0;
}
固件加载机制
设备发现完成后,driver紧随后执行固件(Firmware)加载流程。昇腾加速卡是一种异构计算设备,其上不仅有承担计算任务的加速器核心(通常称为Device端),还有一颗运行固件的嵌入式微控制器(通常称为MCU或BSP)。Host端driver负责将固件二进制文件加载到加速卡的SRAM或DRAM中,并触发固件启动。固件一旦运行起来,就会与Host端driver建立通信通道,共同管理加速卡的运行状态。
固件文件在文件系统中通常以.elf或.bin格式存储,由CANN安装包提供。driver在加载固件时优先通过固件子系统(firmware subsystem)的标准接口请求固件二进制数据。固件子系统的优势在于它支持固件缓存和热缓存机制:如果同一个固件文件已经被加载过,内核会直接从缓存中返回数据,而不需要再次从存储设备读取。固件请求完成后,driver会将固件镜像通过PCIe DMA(Direct Memory Access)通道传输到加速卡的内存中。DMA传输方式避免了CPU逐字节拷贝带来的巨大开销,因为固件镜像的大小通常在数MB级别。
Host端与Device端握手协议
Host端驱动与Device端固件之间的握手协议是设备初始化流程中最核心的通信机制。握手协议定义了Host与Device之间的命令格式、应答规范、超时处理和错误恢复策略。由于握手协议涉及两端运行的软件之间的跨边界通信,协议设计需要充分考虑PCIe通信的不可靠性和延迟不确定性。
握手流程的起始环节是中断信号交换。当固件完成自身初始化后,会向Host端发送一个MSI(Message Signaled Interrupt)中断,告知Host端固件已经就绪。Host端driver在probe阶段已经注册了中断处理函数,该函数在收到固件的READY中断后会在中断上下文中读取设备状态寄存器,确认固件确实处于就绪状态,此后立即触发下半部(bottom-half)处理来完成后续初始化工作。
资源抽象层
内存管理子系统
driver仓的内存管理子系统负责建立Linux内核内存空间与昇腾加速卡DDR内存空间之间的地址映射关系。在昇腾的计算模型中,Host端CPU和Device端加速器各自拥有独立的虚拟地址空间,数据在不同地址空间之间传输时需要经过明确的地址转换。driver通过页表映射机制来管理这种跨地址空间的数据移动。
页表映射机制的核心是在Host端为每个昇腾加速卡维护一个独立的页表结构。这个页表记录了Host端虚拟地址与加速卡物理地址之间的对应关系,以及每个页面的访问权限属性(可读、可写、可执行)。当上层软件发起一个数据搬运操作时,driver会优先检查目标地址是否已经在页表中建立映射,如果尚未建立,则分配新的页面并更新页表条目。页表条目的更新涉及对硬件MMU(Memory Management Unit)寄存器的写入操作,这一步必须严格在原子上下文中执行,以避免并发访问导致页表损坏。
在多进程共享加速卡的场景下,driver还需要处理地址空间隔离问题。Linux内核通过为每个进程维护独立的页表副本来实现进程间的地址隔离。对于昇腾加速卡而言,driver采用了类似的策略:为每个打开设备文件的进程维护独立的地址空间上下文。当进程切换时,driver会切换加速卡的地址空间寄存器,使得每个进程只能访问自己被授权的内存区域。
内存池管理是driver内存子系统的另一个重要功能。为了减少频繁分配和释放带来的内存碎片和性能开销,driver在初始化阶段会预分配多个不同规格的内存池。上层软件在申请内存时,driver会从最接近请求大小的内存池中分配空闲块。只有当所有预分配内存池都无法满足请求时,driver才会调用底层的页分配器申请新的物理页面。
中断控制器配置与中断路由
昇腾加速卡通过PCIe MSI机制向Host端发送中断信号。MSI是一种优于传统线中断(wire-based interrupt)的中断方式,它利用PCIe消息事务传递中断信息,不需要专门的物理中断线路,支持更多的中断向量且具有更好的可扩展性。driver在probe阶段会向PCIe子系统请求分配中断向量,此后将分配到的向量注册到Linux内核的中断管理子系统。
在加速卡内部,中断控制器负责收集来自各个硬件子模块的中断请求(如计算单元完成中断、DMA传输完成中断、错误中断等),并将它们聚合后通过MSI通道发送到Host端。driver需要对加速卡内部的中断控制器进行编程配置,设置每个硬件中断源的目标MSI向量和触发条件。中断优先级和屏蔽位的配置同样由driver负责,合理的优先级配置可以确保关键中断(如错误中断)能够及时被处理,同时避免低优先级中断过度抢占CPU资源。
中断路由策略在多卡系统中尤为重要。当服务器配置了多张昇腾加速卡时,每张卡都会产生中断信号并发送到Host端的中断控制器。driver需要确保每张卡的中断被正确路由到对应的CPU核心,避免所有中断集中在同一个CPU核心上造成性能瓶颈。现代Linux内核支持中断亲和性(IRQ affinity)配置,driver会查询系统的CPU拓扑信息,为每张加速卡分配最优的中断目标核心。
多设备场景下的资源分配策略
在大型数据中心和AI训练集群中,单台服务器通常配置多张昇腾加速卡以提供更大的总算力。driver必须支持多设备(Multi-DIE、Multi-chip)场景下的资源分配和协调工作。多设备场景的资源管理挑战主要体现在两个方面:一是对每张卡的独立资源进行隔离管理,二是对跨卡资源进行统一的调度协调。
driver通过设备索引机制来区分不同的昇腾加速卡。每张加速卡在probe阶段会被分配一个唯一的逻辑设备编号,编号从0开始依次递增。这个设备编号贯穿driver的所有管理数据结构,上层软件通过设备编号来指定操作的目标加速卡。在内存分配方面,driver为每张卡维护独立的内存池,内存池之间严格隔离,一张卡无法访问另一张卡的内存空间。
在跨设备同步方面,driver提供了跨设备事件(Cross-Device Event)机制。当一个计算任务需要在多张卡上协同执行时,上层软件可以通过driver的跨设备事件接口来协调各卡的执行进度。跨设备事件的底层实现依赖于PCIe的原子操作(Atomic Operation)特性,确保在多卡并发访问共享状态时不会发生数据竞争。
NUMA(Non-Uniform Memory Access)亲和性是多设备资源分配中的重要考量。当服务器采用多路CPU架构时,每张昇腾加速卡通过PCIe插槽与特定的CPU插槽连接。driver在初始化阶段会检测每张卡与各CPU插槽的物理连接关系,并在内存分配时优先选择与目标加速卡位于同一NUMA节点的CPU内存。这种NUMA感知的内存分配策略可以显著减少跨NUMA节点的数据访问延迟。
以下代码段展示了多设备资源分配中设备发现和NUMA亲和性绑定的核心逻辑。
static int ascend_allocate_device_context(int slot_index)
{
struct ascend_context *ctx;
struct pci_dev *pdev;
int numa_node;
pdev = ascend_get_pdev_by_slot(slot_index);
if (!pdev)
return -ENODEV;
numa_node = pcibus_to_node(pdev->bus);
if (numa_node == NUMA_NO_NODE) {
numa_node = 0; /* fallback to node 0 if detection fails */
}
ctx = devm_kzalloc(&pdev->dev, sizeof(*ctx), GFP_KERNEL);
if (!ctx)
return -ENOMEM;
/* bind memory allocations to the NUMA node of the accelerator */
ctx->memblock = devm_memremap_pages(&pdev->dev,
get_dev_pagemap(numa_node), numa_node);
if (IS_ERR(ctx->memblock))
return PTR_ERR(ctx->memblock);
ctx->slot_id = slot_index;
ctx->numa_node = numa_node;
atomic_set(&ctx->refcount, 0);
return 0;
}
效率对比
以下表格从四个关键维度对比昇腾CANN driver在引入前后对系统行为的影响,以及差异背后的真实硬件约束。
| 维度 | 优化前 | 优化后 | 差异来源 |
|---|---|---|---|
| 设备就绪时间 | 驱动加载后约1200ms内完成全部初始化(含DDR训练约800ms、固件加载约200ms、中断路由配置约200ms) | 驱动加载后约500ms内完成全部初始化(固件预验证约50ms、并行DDR训练约300ms、快速中断路由约150ms) | 优化前固件需逐卡串行加载且DDR训练无流水线并行;优化后固件采用批量预验证机制且DDR训练流水线并行化 |
| 多卡扩展 | 每增加一张卡,初始化时间线性增加1200ms;8卡系统总初始化时间约9.6秒 | 每增加一张卡,初始化时间增量降至约350ms(受限于PCIe总线仲裁竞争);8卡系统总初始化时间约2.8秒 | 优化前无NUMA亲和性预判,多卡争用同一中断控制器导致串行化;优化后采用分组并行初始化策略 |
| 中断延迟 | MSI中断从Device到Host平均延迟约8微秒,抖动范围4-25微秒 | MSI中断从Device到Host平均延迟约2.5微秒,抖动范围1-8微秒 | 优化前使用共享中断向量表且GIC(Generic Interrupt Controller)未针对昇腾加速卡进行亲和性优化;优化后为昇腾设备分配独立中断向量并绑定专用CPU核心 |
| 固件兼容性 | 仅支持同一大版本号内的固件更新(如6.3.x系列) | 支持跨小版本的固件热更新,通过签名验证机制确保安全 | 优化前采用硬编码版本校验,版本不匹配直接拒绝加载;优化后引入基于RSA签名的分级校验机制,允许在driver兼容范围内的固件更新 |
结尾
driver仓作为昇腾CANN软件栈的最底层组件,承担着固件管理、设备初始化和资源抽象等核心职责,其技术深度和复杂度远超大多数内核驱动模块。soc模块、ctrl模块和devdrv模块的三层架构设计将芯片级硬件操作、设备控制流编排和标准化接口暴露严格分离,使得driver能够在保持代码结构清晰的同时支持多种型号的昇腾芯片。从PCIe枚举到固件加载再到握手协议完成的完整初始化链路中,每一步都涉及对硬件约束条件的精确处理。内存管理子系统的页表映射机制、中断控制器的配置与路由策略,以及多设备场景下的NUMA感知资源分配方案,共同构成了driver资源抽象层的完整技术体系。
仓库地址:https://atomgit.com/cann/driver
转载自 CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/2302_79499487/article/details/162150220




