【Bug已解决】[Mobile] Generic Android hardware acceleration 解决方案

发布时间:2026/8/15 7:18:49
【Bug已解决】[Mobile] Generic Android hardware acceleration 解决方案 【Bug已解决】[Mobile] Generic Android hardware acceleration 解决方案一、现象长什么样在 Android 上想启用“通用硬件加速”让 ORT 自动挑当前设备可用的硬件加速器而不是手动指定某个具体 EP结果开启后反而出问题# 要么直接崩 E OrtAndroid: generic HW accel: op X not supported by accelerator, no fallback # 要么结果错 InferenceSession(generic_hw) produced mismatched output vs CPU # 要么加速没生效悄无声息退回 CPU但延迟没改善具体表现开启“通用硬件加速”后部分模型在 Android 上崩溃报错指向“某个 op 加速器不支持、且没有回退”。另一些模型不崩但输出和纯 CPU 对不上——说明加速路径对某个 op 的实现和 CPU 不一致。用户本以为“开了通用加速就全自动”结果要么崩、要么错、要么没加速体验比手动指定 EP 还差。在带 NNAPI / GPU 的设备上表现各异有的设备稳有的设备必崩。关键特征“通用硬件加速”应是一层自动分配per-node 选最快可用的 EP、不支持就回退 CPU但实现上要么没做 per-node 回退、要么回退一致性没保证于是崩/错/无效。二、背景Android 上的 ORT 有多种加速后端CPU参考、NNAPIAndroid 神经网络 API可调度到 DSP/GPU/NPU、XNNPACKCPU 上的高度优化内核、以及各芯片厂专属 EP如前面提过的 Qualcomm HTP。“通用 Android 硬件加速”的诉求是开发者不想关心“这台手机用哪家 NPU”希望 ORT 提供一个统一的、自动的加速入口——ORT 自己看图、看设备把能加速的节点分到硬件加速器不能加速的留在 CPU并且保证数值和 CPU 一致。要做到这点核心是逐节点 EP 分配per-node EP assignment 严格回退图优化阶段遍历每个节点问“当前设备的某个硬件加速器能不能正确实现这个 op”能 → 分配到该加速器不能 → 留在 CPU回退。回退的节点必须数值与 CPU 完全一致因为这是“正确性兜底”且回退不能断链加速器节点和 CPU 节点之间数据如何搬运要处理好。问题就出在这层“通用加速”的分配器要么没有真正 per-node 回退一个 op 加速器不支持整图就崩要么回退节点的数值和 CPU 不一致因为加速器输出的布局/精度与 CPU 不同拼接处没转换于是出现崩/错/无效三种现象。三、根因根因是“通用 Android 硬件加速”缺少健全的 per-node 分配与回退机制且回退后的一致性没保证没有 per-node 回退分配器按“整图能否被加速器吃下”判断而不是“逐节点”。一旦有个别 op 加速器不支持没有“把它单独留 CPU”的逻辑整图构造失败 → 崩溃。回退后数值不一致即使做了回退加速器节点输出可能在 NCHW vs NHWC 布局、或 fp16 vs fp32 精度上和 CPU 不同回退节点直接接上没有插入布局/精度转换节点导致结果错位。回退断链/数据搬运缺失加速器与 CPU 之间的张量可能在不同内存比如加速器在专用内存回退节点读取时没做拷贝/同步 → 读到垃圾或崩溃。“加速无效”某些设备上加速器实际不支持该模型任何 op分配器却“声称”加速结果还是全 CPU但多了无谓的分配开销延迟没改善。一句话通用硬件加速的关键不在“开”而在“逐节点分配 正确回退 回退一致性”这三件没做好就会出现崩、错、无效。四、最小可运行复现下面用 Python 模拟“逐节点分配 回退”的机理复现“整图判断导致崩溃”vs“逐节点回退正确”from dataclasses import dataclass from typing import List dataclass class Node: name: str supported_by_accel: bool def assign_whole_graph_buggy(nodes: List[Node]) - str: 错误整图判断任一 op 不支持就崩。 if all(n.supported_by_accel for n in nodes): return all_accel raise RuntimeError(generic HW accel: op not supported, no fallback) def assign_per_node_fixed(nodes: List[Node]) - List[str]: 修复逐节点分配不支持的回退 CPU。 plan [] for n in nodes: plan.append(accel if n.supported_by_accel else CPU) return plan nodes [Node(A, True), Node(B, False), Node(C, True)] try: assign_whole_graph_buggy(nodes) except RuntimeError as e: print(buggy:, e) # 崩溃 print(fixed:, assign_per_node_fixed(nodes)) # [accel,CPU,accel] 正确回退buggy因一个不支持的 op 整图崩fixed把不支持的 B 单独回退 CPU其余走加速——正是通用加速该有的行为。五、解决方案第一层最小直接修复最小修复是实现真正的 per-node EP 分配遍历节点加速器支持的分配过去不支持的留在 CPU并在加速器/CPU 边界插入必要的布局/精度转换与内存同步// android_generic_hw.cpp修复片段 Status AssignGenericHwAccel(Graph graph, const HwCapabilities cap) { for (Node n : graph.Nodes()) { if (cap.Supports(n.OpType(), n.Inputs())) { n.SetExecutionProvider(kAccelEp); // 加速器支持 - 分配 } else { n.SetExecutionProvider(kCpuEp); // 不支持 - 回退 CPU逐节点 } } // 在 accel/CPU 边界插入转换节点保证布局/精度/内存一致 ORT_RETURN_IF_ERROR(InsertBoundaryTransfers(graph)); return Status::OK(); }这一层让“通用硬件加速”真正“能加速的加速、不能的回退 CPU”且回退后数值与 CPU 一致不再崩/错/无效。六、解决方案第二层结构性改进把“通用 Android 硬件加速如何分配、如何回退、一致性如何保证”收口成唯一的配置对象OrtAndroidGenericHwPolicy所有分配逻辑读它from dataclasses import dataclass from typing import Tuple dataclass(frozenTrue) class OrtAndroidGenericHwPolicy: Android 通用硬件加速分配的单一事实来源。 # 逐节点分配不支持的回退 CPU而非整图判断 per_node_assignment: bool True # 回退节点数值必须与 CPU 一致正确性兜底 fallback_must_match_cpu: bool True # 加速器/CPU 边界插入布局/精度/内存转换 insert_boundary_transfers: bool True # 加速器不支持任何 op 时明确走全 CPU不假装加速 no_silent_fake_accel: bool True # 代码评审卡点 forbidden_patterns: Tuple[str, ...] ( whole-graph accel or crash, fallback without transfer nodes, ) def plan(self, supported: tuple) - tuple: if not self.per_node_assignment: raise RuntimeError(whole-graph assignment forbidden) return tuple(accel if s else CPU for s in supported) def describe(self) - str: return 逐节点分配、不支持回退 CPU、边界转换保证一致 POLICY OrtAndroidGenericHwPolicy() def plan_generic_hw(supported: tuple, policy: OrtAndroidGenericHwPolicy POLICY) - tuple: return policy.plan(supported)所有通用加速分配都读POLICYper-node 回退与一致性被固化不会再崩/错/无效。七、解决方案第三层断言 / CI 守护把“逐节点回退、一致性、不假装加速”做成断言。下面用 pytest 守护import pytest def test_per_node_assignment(policy): assert policy.per_node_assignment is True assert policy.plan((True, False, True)) (accel, CPU, accel) def test_fallback_matches_cpu(policy): assert policy.fallback_must_match_cpu is True def test_boundary_transfers(policy): assert policy.insert_boundary_transfers is True assert fallback without transfer nodes in policy.forbidden_patterns def test_no_fake_accel(policy): assert policy.no_silent_fake_accel is True def test_whole_graph_forbidden(policy): assert whole-graph accel or crash in policy.forbidden_patterns这五组断言锁住(1) 逐节点分配(2) 回退一致(3) 边界转换(4) 不假装加速(5) 禁止整图判断。CI 跑通即代表通用 Android 加速不会再崩/错/无效。八、排查清单遇到 Android 通用硬件加速崩/错/无效看是否逐节点回退一有不支持的 op 就整图崩 → 整图判断本题。查分配器是不是遍历每个节点分别分配而非“整图能否加速”。查回退一致性加速器与 CPU 在布局/精度上不同回退边界有没有转换节点。查内存同步加速器专用内存的張量回退到 CPU 时有没有拷贝/同步。改 per-node 边界转换不支持回退 CPU边界插转换保证一致。统一到OrtAndroidGenericHwPolicyCI 断言禁止整图判断、要求一致性。端到端多设备验证“能加速的加速、不能的回退且结果对齐”。九、小结[Mobile] Generic Android hardware acceleration的根因是“通用 Android 硬件加速”本应逐节点把 op 分配到可用加速器、不支持的回退 CPU 并保证数值一致但实现上要么做“整图能否加速”的粗判断一个 op 不支持就整图崩要么回退后没在加速器/CPU 边界插入布局/精度/内存转换结果错位要么加速器实际不支持却假装加速无效。三者对应崩、错、无效三种现象。最小修复是实现真正的 per-node EP 分配 边界转换不支持的回退 CPU 且保证一致结构性改进是用唯一的OrtAndroidGenericHwPolicy固化分配与回退语义CI 用五组断言守护“逐节点、一致性、不假装加速”。记住通用硬件加速的难点从来不是“开”而是“逐节点分配 正确回退 回退一致性”这三件套。

相关新闻