深入 Android 端侧 AI 推理的内存带宽优化:从 GPU 共享内存到 NPU 零拷贝的异构数据传输架构

去年在做端侧实时图像分割时,碰到一个反直觉的问题:骁龙 8 Gen 3 的 NPU 理论算力是上一代的 2 倍,推理延迟却只降了 15%。瓶颈不在计算单元——时间全花在数据搬运上了。

一个 224×224×3 的输入帧,从 Camera HAL 到 App 进程,再到 GPU/NPU,中间要经历 4 次内存拷贝。对实时推理场景,算力经常在等数据,内存带宽才是真正的天花板。

端侧推理的内存困境

一条典型的 Android 推理数据流:

Camera HAL → Gralloc Buffer → App Buffer → GPU Buffer → 推理结果 → App Buffer

每一步拷贝都在消耗带宽和延迟。以 1080p YUV 帧为例,单帧约 3MB,30fps 就是 90MB/s 的持续带宽。再加上中间 tensor 的来回搬运,实际内存流量远超模型理论需求。

Android 的进程隔离机制天然导致这个问题:HAL 层分配的内存(Gralloc)和应用层堆内存(Heap)位于不同虚拟地址空间,跨进程共享需要经过 Binder 传输或 ashmem 映射,而这些路径都存在拷贝。

我在 Pixel 8 Pro 上用 Android Studio Memory Profiler 抓过一条推理链路:

# 简化后的 systrace 片段,注意 buffer 搬运耗时
hal_camera_stream::request_buffer  2.3ms
ION_alloc  1.8ms  
AHardwareBuffer_lock  4.1ms  # 这里最痛
gpu_memcpy_h2d  1.5ms
model_inference  3.2ms  # 反而最少

推理本身只花了 3.2ms,数据准备和搬运占了近 10ms。端侧 AI 优化的主战场从来不该是模型结构,而是数据路径。

GPU 路径:共享内存的第一道防线

Google 在 Android 10 引入的 AHardwareBuffer,本质上是对 Gralloc buffer 的统一抽象。它允许 GPU、CPU、HAL 通过同一个句柄访问物理内存,避免显式拷贝。

基本思路是:Camera HAL 输出的 buffer 直接用 AHardwareBuffer 描述,传给 GPU 推理引擎时不拷贝,通过 EGL Image 或 Vulkan External Memory 直接绑定:

// 关键路径:零拷贝绑定 Gralloc buffer 到 GPU
AHardwareBuffer_Desc desc = {
    .width = 224,
    .height = 224,
    .format = AHARDWAREBUFFER_FORMAT_R8G8B8A8_UNORM,
    .usage = AHARDWAREBUFFER_USAGE_GPU_SAMPLED_IMAGE |
             AHARDWAREBUFFER_USAGE_CPU_READ_OCCASIONALLY,
};
AHardwareBuffer* buffer;
AHardwareBuffer_allocate(&desc, &buffer);

// 直接创建 Vulkan 外部内存,不经过 CPU 拷贝
VkImportAndroidHardwareBufferInfoANDROID importInfo = {
    .buffer = buffer,
};
VkMemoryAllocateInfo allocInfo = {
    .pNext = &importInfo,
};
vkAllocateMemory(device, &allocInfo, nullptr, &memory);

这套方案将 Camera → GPU 的路径从 3 次拷贝压缩到 0 次,延迟降低约 40%。

但有一个绕不开的问题:GPU 算完后,结果 buffer 回到 CPU 做后处理时,需要调 AHardwareBuffer_lock。这个操作会触发 CPU cache flush 和 GPU pipeline stall,高频调用下依然是开销大头。

用共享内存做推理中间层的实践

TFLite GPU Delegate 在 Android 上的内部实现很有意思。它不直接操作 AHardwareBuffer,而是在底层用 ION 分配物理连续内存,通过 VK_ANDROID_external_memory_android_hardware_buffer 扩展映射给 Vulkan。

实际项目中我做过一个优化:模型推理的输入和输出都放在同一个 ION buffer 的不同偏移位置,中间层的 feature map 也复用这块内存。简化后的代码大致如下:

// 单块 ION buffer 承载全链路数据
ion_user_handle_t handle = ion_alloc(fd, total_size, align);
void* mapped = mmap(nullptr, total_size, PROT_READ | PROT_WRITE,
                    MAP_SHARED, handle, 0);

// 各阶段数据偏移
float* input_tensor  = (float*)(mapped + INPUT_OFFSET);
float* conv1_output  = (float*)(mapped + CONV1_OFFSET);  
float* conv2_output  = (float*)(mapped + CONV2_OFFSET);

// GPU 端通过 Vulkan 用同一块物理内存
vkBindImageMemory(device, image, memory, CONV1_OFFSET);

收益很直接:GPU 内部多个 kernel 之间不需要任何 host 端的内存搬运。纹理对象和 buffer 对象共享同一片物理页,对 Mali GPU 这类统一内存架构尤其友好。

但这个方案的代价是 CPU 需要显式管理偏移和同步,代码维护成本不低,而且无法直接适配 NPU。

NPU 零拷贝:绕过 CPU 的终极路径

NPU 的核心优势不是算力密度,而是独立的内存子系统。高通的 Hexagon、联发科的 APU 都有自己的片上 SRAM 和 DMA 引擎,理想的数据流是:

Camera ISP → DDR → NPU SRAM(DMA 直传,CPU 不参与)

高通的 NeuroPilot SDK 提供了这条路径。关键 API 是 HTP(Hexagon Tensor Processor)的 FastRPC 机制,允许直接在 DSP 地址空间分配内存:

// 高通 NPU 零拷贝路径
rpcmem_init();
void* dsp_buffer = rpcmem_alloc(RPCMEM_HEAP_ID_SYSTEM, 
                                 size, RPCMEM_DEFAULT_FLAGS);

// Camera buffer 的物理页直接映射给 DSP
rpcmem_to_fd(dsp_buffer);

// NPU 推理,全程不经过 APPS CPU
hexagon_nn_prepare_input_from_fd(nn_id, dsp_buffer_fd, offset);
hexagon_nn_execute(nn_id);

这套方案将 224×224 输入帧的推理总延迟从 13ms 压到了 5ms 以内——搬数据的时间几乎为零。

但落地有两个现实问题。一是各家 NPU SDK 的 API 完全不统一,高通的 rpcmem 和联发科的 Neuron 是两套体系,换芯片等于重写。二是调试极其困难,DSP 端的 crash 不会在 logcat 中出现,必须用专用的 tracer 才能定位,联调的时间经常是写推理逻辑的 3 倍。

统一内存架构:AHAL 的前景与现实

Android 15 开始推的 AHardwareBuffer 2.0 规范尝试统一这个问题。思路是引入 AHARDWAREBUFFER_USAGE_NPU_READAHARDWAREBUFFER_USAGE_NPU_WRITE 标志位,让 NPU 也成为 buffer 的一等公民。

理想情况下,Camera HAL 分配的 buffer 带上 NPU usage 标志,NPU 驱动就能直接 DMA 读取而不触发 CPU 缺页。但目前只有少数旗舰芯片支持,推广速度取决于 OEM 的驱动适配意愿——而这块通常是优先级最低的。

选型建议

实际落地时,我的选择逻辑是这样:

模型在 50MB 以内、延迟不极端(>10ms),用 TFLite GPU Delegate 加 AHardwareBuffer 零拷贝路径就够。代码可维护性最好,跨芯片兼容性也强。

需要跑端侧大模型或实时视频流(<5ms 延迟),必须上 NPU 专有路径。但要做好心理准备——DSP 联调的时间可能是写推理逻辑的 3 倍。

还有一点踩过的坑:功率预算。NPU 零拷贝方案不仅快,功耗也低。连续推理 30 分钟,高通 NPU 路径的功耗比 GPU 路径低约 40%,因为省掉了 DDR 的频繁读写。对移动设备续航来说,这个差距比延迟差距更关键。

端侧 AI 走到今天,硬件算力基本够用了。把数据搬对地方、少搬几次,才是工程上真正拉开差距的地方。