CANN driver 与上层 runtime 的通信协议与命令队列设计
控制与数据分离、用户态零拷贝、无锁队列、硬件加速同步。通过 ioctl 传递控制命令,通过 mmap 共享命令/事件队列,再辅以 Doorbell 触发机制,构建了一条低延迟、高吞吐的任务提交通路。其命令队列的无锁环形缓冲区设计、多流隔离策略以及对容器化环境的支持,不仅满足了当前大模型训练与推理的需求,也为未来更复杂的 AI 工作负载提供了坚实基础。随着 CANN 生态的持续开源,driver 模
前言
在现代 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; // 单个描述符大小
};
注意:
head和tail均为volatile,确保编译器不优化内存访问顺序。
3.2 无锁入队算法
runtime 在提交任务时,执行以下步骤:
- 读取当前
tail; - 计算下一个可用槽位;
- 写入任务描述符物理地址;
- 原子递增
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 模块将成为开发者理解底层硬件交互、进行系统级性能调优的重要入口。
相关链接:
- CANN 组织主页:https://atomgit.com/cann
- driver 仓库地址:https://atomgit.com/cann/driver
更多推荐


所有评论(0)