前言

在现代 AI 异构计算架构中,驱动层(driver)作为连接操作系统内核与硬件加速器的桥梁,其设计直接影响整个软件栈的性能、稳定性和可扩展性。CANN(Compute Architecture for Neural Networks)作为一套完整的 AI 计算平台,其 driver 模块不仅负责设备初始化、资源管理与中断处理,更关键的是为上层 runtime 提供高效、低延迟的通信通道和任务调度机制。

一、Driver 在 CANN 软件栈中的定位与职责

CANN 的软件架构采用分层设计,driver 位于最底层,直接与硬件设备交互,向上通过标准接口为 runtime 层提供服务。根据 driver 仓库 README 中的架构图,其内部划分为三个主要层次:

  • DCMI 层(DaVinci Card Management Interface):面向用户态的设备管理接口,提供设备枚举、状态查询、复位等操作。
  • HAL 层(Hardware Abstraction Layer):硬件抽象层,屏蔽不同芯片型号的差异,提供统一的寄存器访问、中断注册、电源管理等能力。
  • SDK-driver 层:作为 runtime 与内核驱动之间的适配层,封装 ioctl、mmap 等系统调用,暴露 C/C++ API。

其中,runtime 与 driver 的核心交互集中在 SDK-driver 层,通过 /dev/davinciX 设备节点进行通信。这种设计实现了“用户态轻量、内核态高效”的原则,避免频繁陷入内核,同时保障任务调度的实时性。


二、通信通道:ioctl 与 mmap 的协同机制

CANN driver 与上层 runtime 的通信并非依赖单一通道,而是采用 ioctl + mmap 双通道模型,分别承担控制面数据面的功能。

2.1 控制面:ioctl 命令集

所有设备控制命令(如创建上下文、分配内存、启动任务)均通过 ioctl 系统调用下发。driver 定义了一套完整的命令码(DRIVER_CMD_*),位于 pkg_inc/driver_ioctl.h 中:

// pkg_inc/driver_ioctl.h
#define DRIVER_IOC_MAGIC 'd'
#define DRIVER_CMD_CREATE_CONTEXT     _IOWR(DRIVER_IOC_MAGIC, 0x01, struct create_context_args)
#define DRIVER_CMD_DESTROY_CONTEXT    _IOW (DRIVER_IOC_MAGIC, 0x02, uint64_t)
#define DRIVER_CMD_SUBMIT_TASK        _IOW (DRIVER_IOC_MAGIC, 0x03, struct submit_task_args)
#define DRIVER_CMD_ALLOC_MEMORY       _IOWR(DRIVER_IOC_MAGIC, 0x04, struct alloc_mem_args)
#define DRIVER_CMD_FREE_MEMORY        _IOW (DRIVER_IOC_MAGIC, 0x05, uint64_t)
// ... 其他命令

每个命令对应一个结构体参数,例如 submit_task_args 定义如下:

struct submit_task_args {
    uint64_t context_id;      // 上下文标识
    uint64_t task_desc_phys;  // 任务描述符物理地址
    uint32_t task_size;       // 描述符大小
    uint32_t stream_id;       // 流ID(用于多流并行)
};

注:task_desc_phys 是关键字段,指向由 runtime 预先构建并映射到设备可访问内存的任务描述符。

2.2 数据面:mmap 共享内存区

为避免每次任务提交都拷贝大量元数据,CANN driver 采用 mmap 共享内存机制。runtime 通过 mmap() 将内核预分配的缓冲区映射到用户空间,形成“生产者-消费者”环形队列。

典型场景包括:

  • 命令队列(Command Queue):存放待执行的任务描述符指针。
  • 事件队列(Event Queue):存放任务完成通知、异常信息等。
  • 共享描述符池(Descriptor Pool):预分配的任务描述符内存池。

这部分内存由 driver 在设备初始化时通过 dma_alloc_coherent() 分配,并通过 remap_pfn_range() 映射到用户空间。

示例:runtime 映射命令队列
// runtime 侧伪代码
int fd = open("/dev/davinci0", O_RDWR);
void* cmd_queue = mmap(nullptr, QUEUE_SIZE,
                       PROT_READ | PROT_WRITE,
                       MAP_SHARED, fd, CMD_QUEUE_OFFSET);
// 此后可直接读写 cmd_queue,无需系统调用

这种设计极大降低了任务提交的延迟,尤其适用于高吞吐推理场景。


三、命令队列(Command Queue)的实现机制

命令队列是 CANN driver 实现高效任务调度的核心数据结构。其设计目标是:无锁、低延迟、支持多流并发

3.1 环形缓冲区结构

driver 在内核中为每个设备上下文维护一个或多个环形缓冲区(Ring Buffer)。以 src/sdk_driver/trs/ 目录下的实现为例:

// src/sdk_driver/trs/trs_cmd_queue.h
struct trs_cmd_queue {
    volatile uint32_t head;   // 内核消费位置(由硬件更新)
    volatile uint32_t tail;   // 用户生产位置(由runtime更新)
    uint32_t depth;           // 队列深度(如 4096)
    uint64_t desc_phys_addr;  // 描述符数组物理基地址
    uint32_t desc_size;       // 单个描述符大小
};

注意:headtail 均为 volatile,确保编译器不优化内存访问顺序。

3.2 无锁入队算法

runtime 在提交任务时,执行以下步骤:

  1. 读取当前 tail
  2. 计算下一个可用槽位;
  3. 写入任务描述符物理地址;
  4. 原子递增 tail

关键在于避免 head/tail 回绕冲突。driver 采用“预留一个空槽”策略判断队列满:

// 内核侧:检查队列是否满
static inline bool cmd_queue_full(struct trs_cmd_queue *q) {
    return ((q->tail + 1) % q->depth) == q->head;
}

// 用户侧:提交任务
bool SubmitTaskToQueue(uint64_t task_desc_phys) {
    struct trs_cmd_queue *q = get_cmd_queue();
    uint32_t next_tail = (q->tail + 1) % q->depth;
    if (next_tail == q->head) return false; // 队列满

    // 写入描述符地址(假设 desc_array 是 mmap 映射的数组)
    q->desc_array[q->tail] = task_desc_phys;

    // 内存屏障确保写入顺序
    __sync_synchronize();

    // 原子更新 tail
    __atomic_store_n(&q->tail, next_tail, __ATOMIC_RELEASE);
    return true;
}

此设计完全避免了锁竞争,仅依赖内存屏障保证可见性。

3.3 硬件触发与 Doorbell 机制

当 runtime 更新 tail 后,需通知硬件开始处理新任务。CANN driver 采用 Doorbell 寄存器机制:

  • runtime 在更新 tail 后,向特定 MMIO 地址写入“doorbell 值”(通常为流 ID 或队列 ID);
  • 硬件监听该寄存器,一旦写入即触发 DMA 引擎从命令队列拉取任务。

相关代码位于 src/ascend_hal/hdc/(Host-Device Communication)模块:

// src/ascend_hal/hdc/hdc_doorbell.c
void hdc_ring_doorbell(uint32_t stream_id) {
    writeq(stream_id, hdc_base + DOORBELL_REG_OFFSET);
    // writeq 是 64 位 MMIO 写操作
}

这种“写内存 + 写寄存器”的两步操作,是高性能异构计算的经典范式。


四、同步与事件通知机制

任务提交后,runtime 需等待其完成。CANN driver 提供两种同步方式:

4.1 阻塞等待(Blocking Wait)

通过 ioctl(DRIVER_CMD_WAIT_EVENT) 阻塞当前线程,直到指定事件发生。内核使用 wait queue 实现:

// 内核侧:事件完成回调
void on_task_complete(uint64_t event_id) {
    struct event_waiter *w = find_waiter(event_id);
    if (w) {
        complete(&w->completion); // 唤醒等待线程
    }
}

// ioctl 处理函数
long driver_ioctl_wait_event(struct file *file, unsigned long arg) {
    struct wait_event_args args;
    copy_from_user(&args, (void*)arg, sizeof(args));

    DECLARE_COMPLETION_ONSTACK(comp);
    register_waiter(args.event_id, &comp);

    wait_for_completion(&comp); // 阻塞
    unregister_waiter(args.event_id);
    return 0;
}

4.2 事件队列轮询(Polling)

对于高性能场景,runtime 可选择轮询事件队列:

// 用户态:检查事件队列
struct event_queue *eq = get_event_queue();
if (eq->head != eq->tail) {
    // 有新事件,处理之
    handle_event(eq->events[eq->head]);
    eq->head = (eq->head + 1) % eq->depth;
}

driver 保证 head 由内核更新,tail 由用户更新,同样采用无锁设计。


五、多流(Multi-Stream)与资源隔离

为支持并行任务执行,CANN 引入 Stream 概念。每个 stream 拥有独立的命令队列、事件队列和资源上下文。

driver 通过以下方式实现隔离:

  • Stream ID 作为索引:所有队列按 stream_id 分组;
  • 硬件上下文切换:任务描述符中包含 stream_id,硬件据此隔离寄存器状态;
  • 内存域隔离:通过 SVM(Shared Virtual Memory)或 HMM(Heterogeneous Memory Management)确保不同 stream 的内存访问安全。

相关代码在 src/sdk_driver/vascend/(虚拟算力切分)和 src/ascend_hal/svm/ 中体现。


六、最新演进:容器共享与设备虚拟化支持

截至 2026 年初,driver 仓库新增了对 容器设备共享 的支持(见 PR #35)。通过 npu-smi info -t device-share 可查看共享状态。

其实现关键在于:

  • 内核驱动支持多进程打开同一设备文件;
  • 资源管理模块(DMS)引入引用计数;
  • 命令队列按进程/容器隔离,避免交叉干扰。

这使得 CANN 能在 Kubernetes 等云原生环境中高效部署,进一步拓展其应用场景。


七、总结

CANN driver 与上层 runtime 的通信协议设计体现了高性能异构计算系统的典型架构思想:控制与数据分离、用户态零拷贝、无锁队列、硬件加速同步。通过 ioctl 传递控制命令,通过 mmap 共享命令/事件队列,再辅以 Doorbell 触发机制,构建了一条低延迟、高吞吐的任务提交通路。

其命令队列的无锁环形缓冲区设计、多流隔离策略以及对容器化环境的支持,不仅满足了当前大模型训练与推理的需求,也为未来更复杂的 AI 工作负载提供了坚实基础。随着 CANN 生态的持续开源,driver 模块将成为开发者理解底层硬件交互、进行系统级性能调优的重要入口。


相关链接:

Logo

昇腾计算产业是基于昇腾系列(HUAWEI Ascend)处理器和基础软件构建的全栈 AI计算基础设施、行业应用及服务,https://devpress.csdn.net/organization/setting/general/146749包括昇腾系列处理器、系列硬件、CANN、AI计算框架、应用使能、开发工具链、管理运维工具、行业应用及服务等全产业链

更多推荐