夜雨聆风学习资料网

ARTICLE · 1295151

端侧AI工具链原理(09):Runtime、Delegate与异构执行:零拷贝为什么仍然需要同步

端侧AI工具链原理(09):Runtime、Delegate与异构执行:零拷贝为什么仍然需要同步

本文是端侧 AI 工具链原理第 9 篇。上一篇讨论后端 Code Generation,这一篇把可执行资产放回真实系统:多个处理器如何共享 Buffer、调度子图并保证结果和时序正确。

开篇:没有 memcpy ,不代表没有数据移动

CPU 和 NPU 可以通过共享物理内存访问同一 Buffer ,应用也没有调用memcpy,但数据仍需经过 Cache、NoC、IOMMU 和设备本地存储。生产者完成写入后,消费者还必须看到最新内容。

零拷贝准确含义是“避免额外的软件显式副本”,不是“访问与同步免费”。

一、 Runtime 的核心对象

典型 Runtime 管理:

•Device 与 Context;
•Model/Graph 与 Executable;
•Tensor 与 Buffer;
•Queue 与 Command;
•Event、Fence 和 Semaphore;
•Profiling Session;
•Error 与 Recovery。

这些对象共同定义资源生命周期。一个 Buffer 只有在所有异步消费者完成后才能复用或释放。

二、 Delegate 怎样接管子图

LiteRT Delegate 或 ONNX Runtime Execution Provider 通常执行:

1.查询节点支持;
2.选择可接管子图;
3.将子图编译或转换为后端资产;
4.在图中以 Delegate Node 替换原子图;
5.执行时准备 Buffer 并提交后端。

Delegate 不是一个快速 Kernel,而是 Framework Runtime 与 Vendor Runtime 之间的适配层。它必须翻译 Shape、Layout、Quant 和生命周期语义。

三、异构执行的时间线

假设 CPU 预处理、NPU 推理、GPU 后处理:

串行总时延为:

视频流可通过多 Buffer 形成 Pipeline ,让第n帧在 NPU 执行时,第n+1帧 CPU 预处理。但吞吐提高不代表单帧 Latency 下降, Buffer 数量和端到端 Deadline 必须分别计算。

四、同步包含什么

同步可能涉及:

•Command 完成;
•Cache Clean/Invalidate;
•Ownership Transfer;
•IOMMU 映射;
•Layout 或 DType 转换;
•Queue 间依赖;
•CPU 等待与唤醒。

如果 Runtime 使用阻塞等待,CPU 会空转或频繁睡眠唤醒;如果依赖链表达不完整,又可能读到未完成数据。

五、 Buffer 共享的前提

真正可用的零拷贝需要:

•双方支持相同 Buffer 类型;
•地址可由 IOMMU 映射;
•Alignment 和 Stride 兼容;
•Layout 与 DType 一致;
•Cache Coherency 或显式维护正确;
•生命周期跨异步执行有效;
•安全域和权限允许共享。

只满足“同一物理内存”而 Layout 不同,Runtime 仍可能插入 Reformat Buffer。

六、 Dynamic Shape 由谁负责

Runtime 获取真实输入 Shape 后,可能:

•选择预编译 Bucket;
•触发 JIT 或延迟编译;
•重新规划 Buffer;
•拒绝超出范围的输入;
•回退通用后端。

实时产品应避免首次遇到 Shape 时才执行昂贵编译。可在安装、首次启动或后台空闲时预热常用 Bucket。

七、多模型并发与优先级

手机上相机、语音、系统感知和第三方 App 可能同时请求 NPU。Runtime/Driver 需要处理:

•Context 隔离;
•时间片或任务级抢占;
•Priority 与 Deadline;
•SRAM 和带宽配额;
•电源频率投票;
•错误隔离。

单模型 Benchmark 无法代表并发 QoS。高优先级相机任务到来时,低优先级生成式任务是否能在可控粒度暂停,是产品级能力。

八、 Runtime 错误要可恢复

设备超时、Firmware 崩溃、内存不足和资产版本不匹配都可能发生。Runtime 需要定义:

•本次请求失败还是 Context 失效;
•是否可以重试;
•是否切换 CPU/GPU;
•Buffer 是否仍可信;
•如何收集 Dump 与 Timeline;
•是否影响其他 Client。

“重启 App 解决”不是量产系统的完整策略。

九、 Runtime 性能应怎样测

分解:

同时记录:

•首次加载和稳态提交;
•Queue 深度;
•Fence 等待;
•CPU 占用;
•Buffer 映射次数;
•Power State 切换;
•并发干扰。

Kernel 很快而 Runtime 开销占比高时,应优化资产缓存、Persistent Mapping 和异步提交。

十、一个三段异构流水案例

设视频任务为CPU预处理2 ms、NPU推理10 ms、GPU后处理4 ms,交接各1 ms。串行单帧 Latency 为:

采用三 Buffer 流水后,理想稳态帧周期由 NPU 阶段决定,接近10 ms;但单帧 Latency 仍约18 ms,并且 Buffer 在三个设备间的生命周期更长。

如果 GPU 后处理偶发升至14 ms,Queue 会积压。Runtime 需要设置最大 Queue 深度、 Backpressure 和 Drop 策略。无限增加 Buffer 只能推迟掉帧,同时抬高用户看到的延迟。

对 Queue 深度Q,最坏排队增加近似为:

所以吞吐、Latency 和 Buffer 数量必须联合验收。

十一、多模型调度的基本策略

Runtime 可以使用 FIFO、Priority、Deadline-aware 或 Fair Share。端侧通常需要混合策略:系统相机优先,Always-on任务保留最低服务,第三方长任务限制时间片。

调度决策至少考虑:

•请求 Deadline 和优先级;
•是否可抢占及抢占粒度;
•Context 切换成本;
•Local Memory 与模型权重常驻;
•DDR 带宽;
•Thermal State;
•Client 身份与安全域。

过细抢占提高实时性,却增加 Context 保存成本;只在 Graph 边界抢占简单,但一个大 Graph 可能阻塞高优先级任务。硬件和 Compiler 应提供可控的 Preemption Point。

十二、零拷贝的验证方法

不能根据 API 名称判断零拷贝,应通过四种证据:

1.Buffer 物理标识或 Handle 在生产者、Delegate 和 Backend 间一致;
2.Timeline 没有额外 Copy/Reformat Kernel;
3.DDR 字节随去掉副本而下降;
4.输入尺寸变化时 Handoff 时间不再按字节线性增长。

即使满足四点, Cache Clean/Invalidate 和 IOMMU 映射仍可能存在。报告应写“零显式副本”或“共享物理 Buffer”,而不是笼统写“零数据移动”。

十三、 Runtime 资产缓存怎样治理

缓存 Context 和 Prepared Graph 可以降低首帧,但需要 Cache Key :

Key 缺少 Runtime 或 Shape 会加载不兼容资产。缓存还需容量上限、LRU/优先级、完整性校验、OTA 失效和低内存清理策略。

量产问题中应能区分“命中旧缓存”“重新编译”“资产损坏回退”,否则首帧抖动难以定位。

十四、一次设备超时的恢复序列

假设 NPU 请求超过 Watchdog。Runtime不应立刻释放所有输入 Buffer,因为设备可能仍在访问。安全流程通常包括:

1.标记请求超时,停止提交依赖命令;
2.向 Driver 请求取消或 Reset;
3.等待设备确认或进入隔离状态;
4.完成/失败相关 Fence,唤醒等待者;
5.使 Context 和未完成输出失效;
6.在确认设备不再访问后回收 Buffer;
7.收集错误码、Timeline 和 Dump;
8.根据策略重建 Context 或 Fallback;
9.验证其他 Client 是否受影响。

如果第 6 步早于设备停止访问,可能产生 Use-after-free;如果 Fence 永不完成,Camera 或 App 会整体卡死。错误路径需要和正常路径一样做压力与并发验证。

十五、 Runtime 接口的可测试性

应提供故障注入:分配失败、映射失败、Compile 超时、Execute 超时、设备 Reset、错误输出和 Fence 丢失。每种故障检查错误码、资源计数、后续请求和 Fallback。

Runtime 还应导出 Context 数、注册 Buffer 数、Queue 深度和最后错误,使长期资源泄漏能被自动检测,而不是等进程 OOM 后再分析。

十六、 Framework Runtime 升级时需要验证什么

Delegate 接口、Tensor Ownership、Dynamic Shape 和线程模型都可能随 Framework 版本变化。升级 LiteRT 或 ONNX Runtime 后,即使 Vendor Backend 不变,也要重新验证分区、输入输出绑定、并发、取消和 Profiler 关联。

特别关注“接管成功但边界变化”: Framework 的新 Canonicalization 可能把原来一个连续子图拆成多段,或插入新的 Cast。版本回归应对比 Delegate 节点数量、每段原始节点范围、边界 Tensor 和端到端 Timeline,而不只检查最终输出。

十七、 Android 共享 Buffer 的完整生命周期

以 Camera 产生 AHardwareBuffer、NPU 消费、GPU 显示为例:

1.Camera Framework 从 Pool 取得 Buffer;
2.ISP 写入并返回 Acquire Fence;
3.App/Delegate 导入 Buffer Handle;
4.NPU 等待 Acquire Fence,执行并产生 Release Fence;
5.GPU 等待 NPU Fence 并渲染;
6.所有消费者完成后,Buffer 归还 Pool。

每一步都包含 Ownership。AHardwareBuffer 引用计数只能保证对象未释放,不能自动保证设备写入完成。Fence 负责时序,Usage/Format/Stride 负责解释, IOMMU 和安全域负责访问权限。

如果 NPU 不支持 Camera 原始 Stride,Delegate 可能创建内部连续 Buffer。此时 API 仍接收 AHardwareBuffer,却不是真正零副本。Profiler 和内存统计必须能看见这次转换。

十八、线程模型和回调为什么影响尾延迟

Framework Runtime、Delegate、Vendor Runtime 和 Driver 可能各有 Worker Thread。若回调在持有全局锁时执行,或所有模型共享单一提交线程,小任务会被大任务阻塞。

需要定义:

•API 是否线程安全;
•Context 能否多线程提交;
•Callback 在哪个线程执行;
•是否允许 Callback 再次调用 Runtime;
•Wait 是阻塞、轮询还是 Event;
•销毁 Context 时如何处理在途 Callback;
•Thread Priority 是否继承 App。

尾延迟测试应包含多 Client、CPU 负载和回调耗时。单线程 Microbenchmark 无法暴露锁竞争和优先级反转。

十九、 Context 切换与模型常驻的取舍

多个模型并发时,将全部权重常驻可降低切换,却可能超过内存;按请求加载节省内存,却增加 Load 和功耗状态抖动。

可定义模型i的常驻价值:

高频小模型优先常驻,低频大模型按需加载。 Always-on 语音、相机和 LLM 的策略不同。 Runtime 还需避免两个 Client 各自加载相同模型副本,可在安全允许时共享只读权重或 Context 资产。

二十、 Runtime 安全与多租户

NPU 执行第三方模型时,Buffer 地址、Command 和模型资产都属于不可信输入。Driver/Firmware 应验证地址范围、Command 长度、权限和超时,Context 之间隔离内存与错误。

Profiling 也可能泄露其他 Client 的时序和模型信息,需要权限控制。量产 Runtime 不能为了调试方便向所有 App 开放底层 Counter 或任意 Op Package 加载。

错误恢复粒度应区分 Request、Context、Process 和 Device。一个第三方 Context 失败,不应无条件 Reset 系统相机正在使用的设备。

二十一、 Runtime 验收矩阵

只有正常 Execute 成功,不能代表 Runtime 达到量产完整度。

二十二、一次间歇性超时怎样建立证据链

假设相机模型每万帧出现一次80 ms超时,平均时延仍只有12 ms。如果只保留 API 起止时间,团队只能在 Framework、Runtime、Driver 和 Firmware 之间猜测。

正确做法是为请求分配 Correlation ID,并贯穿 Delegate 提交、Buffer 导入、 Fence 等待、Command 入队、设备执行、中断和 Callback。超时样本至少导出:各阶段时间戳、等待对象、队列深度、设备频率、当前并发 Context、Reset 记录以及关联 Buffer 的 Ownership。

若证据显示60 ms消耗在 Acquire Fence,NPU 本身可能无责,问题应回到上游 ISP 或 Buffer Pool ;若 Command 已入队却长期未开始,需要检查优先级、队列阻塞和设备电源状态;若设备执行完成但 Callback 迟到,则检查 Worker Thread、锁竞争和 CPU 调度。相同的“Runtime 超时”表象,对应完全不同的修复路径。

量产版本可以使用低开销环形 Trace,仅在错误发生时冻结前后窗口。这样既避免持续全量 Profiling 影响性能,又能为低概率故障保留跨层因果链。一个可恢复 Runtime 还应在报告超时后明确资产是否仍可复用、 Context 是否污染、设备是否需要 Reset ,以及在途请求将返回什么错误。

工程检查清单

•Delegate 接管结果是否可见?
•Buffer 是否真的省去副本,还是隐藏了 Reformat?
•Fence 是否覆盖 Cache 与 Ownership 语义?
•异步 Buffer 生命周期是否正确?
•Dynamic Shape 是否会触发在线编译?
•多模型并发是否有 Priority 与 Quota?
•设备错误是否能隔离和恢复?
•是否分别测首次、稳态和并发 Runtime 开销?

结尾: Runtime 把静态计划变成系统行为

编译器决定理想执行计划, Runtime 负责在动态 Shape 、并发、功耗和错误中兑现它。零拷贝、异步和多设备不是 API 标签,而是一组内存与同步契约。

下一篇是本辑收官:如何用 Profiling 、部署 CI 和资产治理,把一次成功 Demo 变成可复现、可升级的工程交付。

参考资料

1.Google AI Edge: LiteRT Delegates[1]
2.ONNX Runtime: Execution Providers[2]
3.Android Hardware Buffer[3]

参考链接

[1] Google AI Edge: LiteRT Delegates: https://ai.google.dev/edge/litert/performance/delegates

[2] ONNX Runtime: Execution Providers: https://onnxruntime.ai/docs/execution-providers/

[3] Android Hardware Buffer: https://developer.android.com/ndk/reference/group/a-hardware-buffer

--------------------------------

我是半川

一个偏爱在芯片底层与光影算法里死磕细节的工程师

回首页,搜索你想要的文档

在首页搜索框输入关键词,快速定位资料

文库首页搜索指引 立即搜索

相关学习资料