Linux 内核 QAIC 驱动深度解析:Qualcomm AIC100 AI 加速器的中断风暴抑制、NNC 协议与用户态 API
QAIC(Qualcomm Cloud AI Accelerators)是 Linux 内核中面向 AIC100 系列 AI 加速卡的内核态驱动(KMD),它管理 PCIe 加速设备、为深度学习推理负载提供 DMA 数据路径,并通过 DRM 风格的 IOCTL 接口向用户态暴露工作负载提交能力。本篇基于内核文档 Documentation/accel/qaic/qaic.rst 与 drivers/accel/qaic/ 源码实现,系统梳理 QAIC 的中断处理策略(IRQ 风暴抑制与单 MSI 回退)、NNC(Neural Network Control)控制协议的分层设计、完整的用户态 uAPI(8 个 IOCTL 及消息结构),以及全部 6 个模块参数的含义、默认值与生效时机,帮助你在虚拟化或裸机环境中正确配置和排障该驱动。
一、驱动定位与模块组成
根据 Kconfig,QAIC 以 DRM_ACCEL_QAIC 配置项启用,编译选项为 "Qualcomm Cloud AI accelerators",依赖 DRM_ACCEL、PCI、HAS_IOMEM 与 MHI_BUS,选择 CRC32 和 WANT_DEV_COREDUMP。若以模块方式编译,模块名为 qaic。从 Makefile 看,模块由以下文件组成:
mhi_controller:MHI(Modem Host Interface)控制器与设备初始化;qaic_control:NNC 控制路径(QSM 消息收发);qaic_data:DMA 数据路径与 DBC 中断处理;qaic_drv:PCI 驱动主入口与 accel 设备注册;qaic_ras/qaic_ssr:RAS 错误上报与子系统重置(Sub-System Reset);qaic_timesync:主机与设备时间同步;qaic_sysfs/qaic_debugfs:可观测性接口。
qaic_drv.c 中定义了三个受支持的 PCI 设备 ID:0xa080(AIC080)、0xa100(AIC100)、0xa110(AIC200),并声明每设备支持 DBC_NUM = 16 个 DMA Bridge Channel(DBC)。qaic.h 中给出了 DBC 寄存区的布局约定:DBC 寄存器基址为 BAR 内偏移 128KB,每个 DBC 占 4KB。
二、中断处理:IRQ 风暴抑制机制
2.1 为什么硬件自带的抑制还不够
AIC100 的 DMA Bridge 硬件内置了 IRQ 风暴抑制:只有当响应 FIFO 从空变为非空时才触发 MSI,向非空队列追加第 N 个事件不会再产生中断。但文档指出,当负载足够快且主机响应足够及时时,主机可以把响应 FIFO 排空的速度跟上设备入队速度,于是 FIFO 频繁在空与非空之间切换,每个元素都对应一次 MSI——lprnet(车牌识别网络)这类负载已知可超过每秒 10 万次 MSI,多数系统无法长期承受,会因中断控制器开销触发看门狗而崩溃。
qaic_data.c 的注释给出了一个实测场景:在 Intel Xeon D-2191 上跑 8 个 lprnet 实例时,/proc/interrupts 显示约 8 万次中断/秒,尽管 NAPI 的设计假设是"主机只管持续排空",但这种行为在部分主机上导致系统不稳定。
2.2 软件层面的"禁用—排空—末次轮询"流程
QAIC 实现了一套类 NAPI 的中断禁用方案(因为没有 netdev,无法直接用 NAPI,改用 threaded IRQ),其流程与文档描述一一对应:
- 收到 IRQ 立即禁用该中断线。dbc_irq_handler() 校验响应队列头尾指针(读到
U32_MAX视为 PCIe link error,返回IRQ_NONE),队列非空时调用disable_irq_nosync(irq)并IRQ_WAKE_THREAD唤醒线程化下半部。 - 线程下半部排空 FIFO。dbc_irq_threaded_fn() 以
NUM_EVENTS = 128个响应条目为一轮循环读取rsp_q_base,按req_id在dbc->xfer_list中匹配 BO,累计每个 BO 已完成的 slice 数(nr_slice_xfer_done),全部 slice 完成后执行dma_sync_sgtable_for_cpu()、记录req_processed_ts、complete_all(&bo->xfer_done)唤醒等待者,最后把新的 head 写回设备。 - "last chance" 轮询。FIFO 排空后并非立刻重新使能中断,而是保留中断禁用状态、睡眠等待更多活动:代码中
NUM_DELAYS = 10次、每次usleep_range(100, 200)(即最长约 1~2ms 的观察窗口)。期间若仍有数据入队则继续处理。 - 无活动才重新使能中断线。线程在
normal_out处enable_irq(irq);同时注释特别提示"检查 FIFO 与使能中断之间存在竞争",因此使能后会再读一次 tail,若发现新数据则重新disable_irq_nosync并回到排空循环,堵住漏事件窗口。
这套方案的收益在文档与源码注释中一致:同一 lprnet 场景从约 8 万~10 万 IRQ/秒降到 5 分钟约 64 次 IRQ,主机保持稳定,且工作负载吞吐不变(在逐次运行噪声范围内)。
线程下半部中对 BO 完成判定的关键约束值得注意:一个 BO 可被切成多个 slice,每个 slice 产生独立中断,因此只有 nr_slice_xfer_done 达到 nr_slice 时才算该 BO 执行完毕(见 qaic_data.c#L1645-L1671)。
三、单 MSI 回退模式(Single MSI Mode)
3.1 背景:MultiMSI 在虚拟化环境下的困境
文档指出(circa 2023),MultiMSI 并非所有系统都良好支持,虚拟化环境尤其如此——hypervisor 可能屏蔽 PCIe MSI capability 结构,而支持 MultiMSI 所需的 vIOMMU 又有较大的内存开销。因此 QAIC 支持"只分配到 1 个 MSI"的降级路径:设备检测到仅配置了 1 个 MSI 后,会把所有 DBC 的中断导向原本用于 MHI 的那条中断线,即 MHI 与全部 DBC 共享同一个 MSI。
这在源码中由 qaic.h 的 struct qaic_device.single_msi 标志承载,并在中断路径中多处判断:
dbc_irq_handler()在single_msi为真时不执行disable_irq_nosync()(qaic_data.c#L1530-L1533),因为这条线与 MHI 共享,禁用会同时杀死控制路径;- 线程下半部退出时,
normal_out仅在!single_msi && !datapath_polling时才enable_irq()(qaic_data.c#L1686-L1706)。
3.2 共享中断的唤醒语义与副作用
由于单条 MSI 被共享,每次中断到达时所有 DBC 与 MHI 的 handler 都会被唤醒;但 DBC 的线程化 handler 仅在检测到待处理工作(队列非空)时才真正开始,而 MHI 的线程 handler 总是会被启动——这与文档描述一致,是一种"唤醒代价可接受、真实工作按需在位"的折中。
需要特别注意文档提示的副作用:在单 MSI 模式下,软件 IRQ 风暴抑制被绕过了。因为 MSI 共享、永远不能被禁用,响应 FIFO 每新增一个条目都会触发一次新中断,风暴场景下的抑制能力退化为纯硬件抑制。这意味着共享 MSI 主机上若跑高吞吐负载,需要自行评估中断速率。
3.3 与 datapath_polling 的交互
datapath_polling 模式下正常不使能 IRQ;但当单 MSI 与轮询同时开启时(共享线无法禁用),dbc_irq_handler() 直接 IRQ_HANDLED 尽快返回,把处理留给轮询线程。轮询由 qaic_irq_polling_work() 实现:循环读取 head/tail,发现 head != tail 就 irq_wake_thread() 唤醒线程下半部;空队列时 usleep_range(datapath_poll_interval_us, 2 * datapath_poll_interval_us) 后重试;设备 OFFLINE、DBC 无用户或 xfer_list 为空时退出。提交路径上,qaic_data.c#L1430-L1431 在入队完成后按 datapath_polling 调度 dbc->poll_work。
四、NNC(Neural Network Control)协议
4.1 KMD 与 UMD 的职责切分
NNC 协议实现被拆分在 KMD(QAIC)与 UMD(用户态驱动)之间,边界非常清晰:
- QAIC 理解:NNC 线协议(wire protocol)的编解码、消息结构与所有事务(transactions),以及需要内核态知识才能处理的部分(例如把主机内存映射为设备 IOVA);
- QAIC 不理解:命令(commands),即 passthrough 事务的 payload——这部分由 UMD 负责;
- 字节序与对齐:QAIC 在其能力范围内强制执行小端序与 64 位对齐;由于 passthrough 内容对 QAIC 不透明,这些要求依赖 UMD 自行满足(qaic_accel.h 的
struct qaic_manage_trans_passthrough注释同样强调 "Opaque to the driver. Userspace must encode in little endian and align/pad to 64-bit")。
4.2 事务类型与 wire 结构
uapi 头文件 定义了完整的事务类型枚举,覆盖 passthrough(用户态/设备双向)、DMA 传输、activate/deactivate、status 查询、terminate 与 partition 校验:
#define QAIC_TRANS_UNDEFINED 0
#define QAIC_TRANS_PASSTHROUGH_FROM_USR 1
...
#define QAIC_TRANS_DMA_XFER_FROM_USR 5
#define QAIC_TRANS_ACTIVATE_FROM_USR 7
...
#define QAIC_TRANS_TERMINATE_FROM_DEV 16
#define QAIC_TRANS_TERMINATE_TO_DEV 17
#define QAIC_TRANS_DMA_XFER_CONT 18
每条事务以 struct qaic_manage_trans_hdr(type + len)开头,整条消息由 struct qaic_manage_msg 描述(len、count、data 指向事务数组),单条消息上限 4KB(QAIC_MANAGE_MAX_MSG_LENGTH)。activate 事务还携带 queue_size(DBC 请求/响应队列的元素数)与 options,响应中带回分配的 dbc_id(qaic_accel.h#L93-L120)。
4.3 terminate 事务:用户态资源回收的关键
文档特别强调了 terminate 事务的作用:QAIC 不感知加载到设备上的资源(大部分活动发生在 NNC 命令内部),因此无法回滚用户态行为。为保证进程崩溃或出 bug 时资源被完全释放,QAIC 在用户离开时发送 terminate 事务通知 QSM(Qualcomm System Management 固件代理):"这个用户走了,可以释放其资源"。
源码印证:qaic_release_usr() 构造 struct wire_trans_terminate_to_dev(qaic_control.c#L147-L151,内含 handle),类型置为 QAIC_TRANS_TERMINATE_TO_DEV,经 MHI 控制通道发出。wire 层结构(如 wire_trans_terminate_to_dev)均以 __packed 加 little-endian 字段(__le32 + cpu_to_le32())组织,正对应文档所述"KMD 负责端到字节序与对齐"。
4.4 NNC 协议版本号
QSM 可上报其支持的 NNC 协议版本,分 Major 与 Minor 两个号,演进规则:
| 变更 | 含义 | 是否影响 QAIC |
|---|---|---|
| Major 变化 | 消息格式或事务(transaction)变更 | 是,影响 QAIC |
| Minor 变化 | 命令(command)变更 | 否,不影响 QAIC |
status 响应结构 struct qaic_manage_trans_status_from_dev 中带出 major/minor,status_flags 的 bit 0 指示是否需要 CRC。QAIC 的 CRC 校验通过 qdev->gen_crc/valid_crc 函数指针注入(qaic.h#L172-L175),get_cntl_version() 用于读取版本(qaic.h#L319)。
五、用户态 API(uAPI)
5.1 accel 设备与 ONLINE/OFFLINE 通告
QAIC 为每个物理 PCIe 设备创建一个 accel device(基于 DRM_ACCEL 框架),该设备在 PCIe 设备被 Linux 知晓的整个生命周期内存在。但设备并不总是可接受请求的状态:QAIC 通过 KOBJ_ONLINE / KOBJ_OFFLINE uevent 通告设备何时可以接受请求、何时因 reset 或其他状态转换停止接受请求。
源码位置:上线时在 qaic_drv.c#L360-L361 设置 dev_state = QAIC_ONLINE 后发 kobject_uevent(KOBJ_ONLINE);qaic_notify_reset() 则发 KOBJ_OFFLINE、置 QAIC_OFFLINE,并唤醒所有控制等待者与全部 DBC 的等待者(避免同步时卡到超时)。设备状态枚举为 QAIC_OFFLINE / QAIC_BOOT / QAIC_ONLINE(qaic.h#L41-L48)。用户态应监听 accel 设备的 uevent 再决定是否提交请求。
5.2 八个 IOCTL 全解
QAIC 在 include/uapi/drm/qaic_accel.h 定义了 8 个驱动专属 IOCTL(命令号 DRM_COMMAND_BASE + 0x00 ~ 0x08)。以下按文档语义结合 uAPI 结构逐一说明:
DRM_IOCTL_QAIC_MANAGE(struct qaic_manage_msg)
向 QSM 发送 NNC 请求的通道。调用阻塞直到收到响应或请求超时(默认 control_resp_timeout_s = 60s)。消息上限 4KB。
DRM_IOCTL_QAIC_CREATE_BO(struct qaic_create_bo)
分配一个可收发工作负载数据的 buffer object(BO),返回 GEM handle。注意:BO 在完成 slicing 之前不可用(见 ATTACH_SLICE_BO)。
DRM_IOCTL_QAIC_MMAP_BO(struct qaic_mmap_bo)
把已分配的 BO 准备成可 mmap() 进用户态进程的形式,返回的 offset 用于 mmap() 调用。
DRM_IOCTL_QAIC_ATTACH_SLICE_BO(struct qaic_attach_slice)
对 BO 做切片(slicing),描述 BO 的哪些部分被送往工作负载的哪个位置。每个 slice 由 struct qaic_attach_slice_entry 描述,字段包括:size/offset(slice 在 BO 内的区间)、dev_addr(推入或从设备拉取的地址)、至多 4 个信号量命令(struct qaic_sem,支持 NOP/INIT/INC/DEC/WAIT_EQUAL/WAIT_GT_EQ/WAIT_GT_0 及 pre/post-sync 标志,见 qaic_accel.h#L19-L30)、门铃 db_addr/db_data/db_len(32/16/8 位,0 表示不敲门铃)。头结构 struct qaic_attach_slice_hdr 指定 dbc_id(该操作需要一组 DMA Bridge 传输,因此把 BO 锁定到指定 DBC)与传输方向 dir(1=DMA_TO_DEVICE,2=DMA_FROM_DEVICE)。
DRM_IOCTL_QAIC_EXECUTE_BO(struct qaic_execute)
向设备提交一组已切片的 BO。调用非阻塞:成功仅代表 BO 已入队,不保证已执行。struct qaic_execute_hdr 含 count 与 dbc_id。
DRM_IOCTL_QAIC_PARTIAL_EXECUTE_BO
行为同 EXECUTE_BO,但允许本次调用缩小 BO 的传输量:若 BO 通常有 N 个输入而当前只有子集可用,可指定只发送前 M 字节(struct qaic_partial_execute_entry 的 resize 字段,必须 ≤ 原 BO 大小;resize = 0 表示本次不涉及 DMA 传输)。该 IOCTL 会动态重算切片,因此入队前有额外处理开销。
DRM_IOCTL_QAIC_WAIT_BO(struct qaic_wait)
判断某个 BO 是否已被设备处理完。阻塞直到 BO 处理完成、可重新入队,或超时(timeout 单位 ms,缺省值由模块参数 wait_exec_default_timeout_ms 决定,调用内显式指定则覆盖默认值)。BO 完成的内核侧信号正是第二、三节描述的 complete_all(&bo->xfer_done)。
DRM_IOCTL_QAIC_PERF_STATS_BO(struct qaic_perf_stats)
采集 BO 最近一次执行的性能统计,供用户态构建端到端处理时间线。返回的 struct qaic_perf_stats_entry 包含:queue_level_before(提交前队列中已有元素数)、num_queue_element(本 BO 占用的队列元素数)、submit_latency_us(驱动提交耗时)、device_latency_us(设备执行耗时)。内核侧统计时间戳来自 struct qaic_bo.perf_stats 的 req_received_ts / req_submit_ts / req_processed_ts,分别在 ioctl 入口、入队完成和中断处理完成时打点。
DRM_IOCTL_QAIC_DETACH_SLICE_BO(struct qaic_detach_slice)
移除 BO 上由 ATTACH_SLICE_BO 提供的切片信息,是 attach 的逆操作。要求 BO 处于空闲状态。detach 成功后 BO 必须先重新 attach 才能执行;attach/detach 组合使用,可以让同一个 BO 服务多个工作负载。
5.3 典型控制流
把上述 API 串起来,一个完整的用户态控制面流程为:
open()accel 设备(每个 open 实例即一个隔离 client);DRM_IOCTL_QAIC_MANAGE下发 STATUS 事务查询 NNC 版本、再发 ACTIVATE 事务获取dbc_id;CREATE_BO→ATTACH_SLICE_BO(绑定 DBC 与切片)→MMAP_BO填入数据;EXECUTE_BO(或PARTIAL_EXECUTE_BO)→WAIT_BO等待完成 →PERF_STATS_BO取统计;- 切换负载时
DETACH_SLICE_BO后重新 attach;deactivate 事务归还 DBC。
六、用户态客户端隔离(User Isolation)
AIC100 支持多客户端:单个客户端可独占多个 DBC,多个客户端也可各自拥有一个或多个 DBC。由于工作负载可能含敏感信息,只有拥有该工作负载的客户端才被允许操作对应 DBC。
隔离机制在源码中体现为:
- 客户端由
open()关联的实例标识——struct qaic_user 持有唯一的handle(kref 计数管理生命周期); - 每个 DBC 记录其持有者
struct dma_bridge_chan.usr(qaic.h#L101-L102),中断 handler 对!dbc->usr的通道直接IRQ_HANDLED忽略(qaic_data.c#L1508-L1511); - 文档的约束——"客户端只能使用自己分配的内存和被分配给自己工作负载的 DBC,访问他人资源的尝试会被拒绝"——在 DBC 释放路径上同样落实:disable_dbc() 校验调用者 handle 与 DBC 持有者一致,不一致返回
-EPERM,一致则清掉usr并synchronize_srcu等待在途访问者退出。
七、模块参数
QAIC 支持 6 个模块参数,默认值与生效时机的文档说明与源码中的 module_param 声明完全一致:
| 参数 | 类型/默认值 | 说明 | 生效时机 | 源码位置 |
|---|---|---|---|---|
datapath_polling |
bool,默认 0(关) | 用轮询线程处理数据路径事件,代替设备中断;适用于 multiMSI 损坏的平台 | 必须在驱动初始化时设置 | qaic_drv.c#L80-L82 |
mhi_timeout_ms |
uint,默认 2000ms | MHI 操作的超时时间 | 必须在驱动检测到设备时设置 | mhi_controller.c#L19-L21 |
control_resp_timeout_s |
uint,默认 60s | QSM 对 NNC 消息响应的超时 | 必须在驱动向 QSM 发送请求时设置 | qaic_control.c#L39-L41 |
wait_exec_default_timeout_ms |
uint,默认 5000ms | WAIT_BO ioctl 的默认超时 |
必须在调用 wait_exec ioctl 之前设置;ioctl 内指定值会覆盖 | qaic_data.c#L57-L59 |
datapath_poll_interval_us |
uint,默认 100us | 数据路径轮询激活时的轮询间隔 | 下一个轮询周期生效 | qaic_data.c#L61-L64 |
timesync_delay_ms |
uint,默认 1000ms | 两次连续 timesync 操作的时间间隔 | — | qaic_timesync.c#L20-L22 |
补充两点源码级细节:
datapath_poll_interval_us的实际用法是 qaic_irq_polling_work() 中的usleep_range(interval, 2*interval),即轮询睡眠区间为 [100us, 200us](默认配置下),与线程下半部"last chance"窗口的usleep_range(100, 200)数值上相同但属于两条独立路径;- 除
datapath_polling权限为0400外,其余数值参数均为0600,即仅 root 可修改;"必须在初始化/检测时设置"的参数意味着热修改对已初始化的设备无效,应通过模块加载参数(如modprobe qaic datapath_polling=1)或重启加载来生效。
八、配置建议与排障要点
结合文档结论与源码实现,实际部署时可参考以下判断:
- 虚拟化环境:若 hypervisor 屏蔽 MSI capability 或 vIOMMU 内存开销不可接受,让驱动走单 MSI 回退即可——设备自行检测 MSI 数量,无需专门开关;但要知道单 MSI 下软件风暴抑制失效(第三、二节),高吞吐负载需评估中断频率。
- multiMSI 损坏的平台:启用
datapath_polling=1,必要时调大datapath_poll_interval_us降低空转开销;注意该参数必须初始化前设置。 - 超时调优:MHI 通道慢/设备恢复慢时调大
mhi_timeout_ms(默认 2s);QSM 响应慢时调大control_resp_timeout_s(默认 60s);推理迭代长时把wait_exec_default_timeout_ms(默认 5s)提到大于单批推理时延。 - 观察中断是否被风暴:以
/proc/interrupts对比设备中断计数速率——启用软件抑制后应接近"5 分钟数十次"量级而非每秒数万;若接近后者,检查是否处于单 MSI 共享模式。 - 设备状态联动:用户态应监听 accel 设备的 KOBJ_ONLINE/OFFLINE uevent,OFFLINE 时驱动会唤醒全部等待者并清空 DBC 上下文(qaic_notify_reset()),避免用户态挂死在 WAIT/控制调用上。
参考路径
- 文档主体:QAIC driver
- 驱动实现:drivers/accel/qaic/qaic_data.c、drivers/accel/qaic/qaic_control.c、drivers/accel/qaic/qaic_drv.c、drivers/accel/qaic/qaic.h、drivers/accel/qaic/mhi_controller.c、drivers/accel/qaic/qaic_timesync.c
- uAPI 定义:include/uapi/drm/qaic_accel.h
- 构建配置:drivers/accel/qaic/Kconfig、drivers/accel/qaic/Makefile
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0622
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00