ADR-004: Adreno GPU 争抢问题分析与解决方案

状态: 已接受 · 优先级: P0 · 最后更新: 2026-06-15


1. 背景:PicMe 的 GPU 消费者

PicMe 是一个强视觉应用,同时使用 GPU 进行实时美颜渲染端侧 LLM 推理。当前应用中有三个 GPU 消费者:

┌──────────────────────────────────────────────────────────────────────┐
│                          GPU 消费者分布                                │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  1. OpenGL ES 渲染管线 (beauty-engine)                               │
│     ├── Camera Preview → SurfaceView (30fps 实时)                    │
│     ├── BeautyRenderer 多 Pass(磨皮/美白/美型/妆容/滤镜)           │
│     ├── PhotoProcessorImpl 离屏渲染(拍照)                          │
│     └── 线程: 独立渲染线程 + EGL 上下文                              │
│                                                                      │
│  2. ncnn Vulkan 计算 (beauty-engine)                                 │
│     ├── 人脸检测推理(Vulkan compute shader)                         │
│     ├── net_.opt.use_vulkan_compute = true                           │
│     └── 线程: 推理线程池                                              │
│                                                                      │
│  3. LLM 推理 (agent-core)                                            │
│     ├── 引擎: llama.cpp / ggml                                       │
│     ├── 后端选择: ggml-cpu (当前) / ggml-vulkan (曾尝试)              │
│     └── 线程: LLM-Model-Thread (专用线程)                            │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

关键约束:三者运行在同一块 Adreno GPU 上,且 Android 的 GPU 调度器 (kgsl) 不支持跨 API 的细粒度优先级抢占。


2. 事件记录

事件 1: MNN Vulkan 与 OpenGL ES 冲突(~2026-05)

现象

MNN-LLM 使用 Vulkan 后端进行 LLM 推理时,出现 vk::DeviceLostError,与当前事件 2 类似。

当时的环境

组件 GPU 使用 备注
OpenGL ES 渲染管线 ✅ 持续占用 Camera preview + Beauty Shader
ncnn Vulkan ✅ 持续占用 人脸检测
MNN Vulkan (LLM) ✅ 新增 旧版 LLM 引擎,后迁移至 llama.cpp
ggml-vulkan ❌ 未引入 llama.cpp 尚未接入

当时的应对

CMakeLists.txt 中以注释记录:

# ggml-vulkan 已禁用 — 与 MNN 的 GPU 上下文冲突 (Adreno 830)

后续 MNN 整体从仓库移除(包括 MNN-LLM 和 MNN 人脸检测),团队认为移除 MNN 后 GPU 冲突问题会自然解决。


事件 2: ggml-vulkan + ncnn Vulkan + EGL 三方争抢(2026-06-15)

触发条件

MNN 移除后,重新为 llama.cpp 启用 GGML_VULKAN=ON,编译了 libggml-vulkan.so(79MB)。

崩溃日志关键信息

19:07:01.726 Fence: waitForever: fence 226 didn't signal in 3000 ms        ← GPU fence 超时
19:07:01.726 Fence: Throttling EGL Production: fence 225 didn't signal      ← EGL 渲染被节流
19:07:04.001 HWUI: Davey! duration=5279ms                                   ← 主线程卡顿 5 秒
19:07:04.008 Choreographer: Skipped 255 frames!                             ← UI 丢帧 255 帧
19:07:04.027 libggml-vulkan: vk::Queue::submit: ErrorDeviceLost             ← Vulkan 设备丢失
19:07:04.028 libc: Fatal signal 6 (SIGABRT)                                 ← 进程崩溃

崩溃链路

ncnn Vulkan compute 持续运行 (每帧人脸检测)
        +
OpenGL ES 渲染持续运行 (30fps 预览 + 美颜)
        +
ggml-vulkan submit LLM 计算任务
        ↓
GPU 过载 → fence 超时 (3s)
        ↓
kgsl 驱动重置 GPU
        ↓
vk::Queue::submit 返回 VK_ERROR_DEVICE_LOST
        ↓
libggml-vulkan C++ 异常 → SIGABRT

根因分析

因素 说明
直接原因 vk::Queue::submit: ErrorDeviceLost,Vulkan 设备被 GPU 驱动重置
根本原因 Adreno 830 (Xiaomi 15) 的 kgsl 驱动无法同时协调 3 个 GPU 上下文(EGL + ncnn Vulkan + ggml Vulkan)
触发条件 LLM 推理(ggml-vulkan)在渲染 + 人脸检测的 GPU 负载高峰时提交计算任务
本质问题 移动端 GPU 的多个上下文(OpenGL ES + Vulkan 多实例)并发访问同一硬件时,Adreno 驱动缺乏可靠的时间片调度机制

3. 决策:LLM 推理使用 CPU-only,GPU 仅用于渲染管线

3.1 决策内容

所有 LLM 推理(llama.cpp)强制使用 CPU 后端,禁止启用任何 GPU 加速后端。 GPU 资源完全留给 beauty-engine 的渲染管线(OpenGL ES + ncnn Vulkan)。

3.2 决策理由

理由 说明
稳定性优先 GPU 争抢导致的 DeviceLost 是致命的(进程崩溃),而 CPU 推理只是速度稍慢
Qwen2.5 0.5B 模型小 0.5B 参数量在 CPU 上 + KleidiAI + NEON DOTPROD 已足够流畅
渲染不可降级 美颜渲染的实时性(30fps + <100ms 交互延迟)比 LLM 推理速度更关键
APK 体积收益 移除 libggml-vulkan.so(79MB),APK 减少 ~79MB
ncnn Vulkan 不可降级 人脸检测是每帧必需的,切换到 CPU 会显著增加推理耗时

3.3 不采纳方案

方案 拒绝原因
让 ncnn 用 CPU 人脸检测每帧都要跑,CPU 推理耗时翻倍,影响 30fps 实时性
分时使用 GPU 需要跨进程/跨 API 同步,Android 无标准方案,复杂度高
多个 Vulkan 实例共享 VkDevice ncnn 和 ggml 各自管理自己的 VkDevice,无法共享
降低 ggml-vulkan 优先级 Adreno 驱动不支持 Vulkan 队列优先级控制

4. 技术实现

4.1 编译配置(CPU-only + NEON)

# llama.cpp Android NDK 编译(Vulkan=OFF)
cmake \
    -DCMAKE_TOOLCHAIN_FILE=$ANDROID_NDK/build/cmake/android.toolchain.cmake \
    -DANDROID_ABI=arm64-v8a \
    -DANDROID_PLATFORM=android-28 \
    -DBUILD_SHARED_LIBS=ON \
    -DCMAKE_BUILD_TYPE=Release \
    -DGGML_VULKAN=OFF \              # ← 关键:禁用 Vulkan
    -DGGML_CPU=ON \
    -DGGML_CPU_KLEIDIAI=ON \         # KleidiAI 矩阵乘优化
    -DGGML_CPU_REPACK=ON \
    -DGGML_LLAMAFILE=ON \            # 快速 token 生成
    -DGGML_OPENMP=OFF \
    -DCMAKE_C_FLAGS="-march=armv8.2-a+dotprod" \
    -DCMAKE_CXX_FLAGS="-march=armv8.2-a+dotprod"

4.2 CMakeLists.txt 变更

# 移除 ggml_vulkan 导入(原样)
- set(GGML_VULKAN_SO_PATH "...")
- add_library(ggml_vulkan SHARED IMPORTED)
- target_link_libraries(agent_native ggml_vulkan)
- target_compile_definitions(agent_native GGML_USE_VULKAN=1)

# 保留 CPU 优化
target_compile_definitions(agent_native PRIVATE
    GGML_USE_CPU_KLEIDIAI=1
    GGML_USE_LLAMAFILE=1
)

4.3 jniLibs 产物变更

文件 大小 (旧) 大小 (新) 变化
libllama.so 34MB 34MB
libggml-vulkan.so 79MB 已删除 📉
libggml-cpu.so 5.2MB 5.2MB
libggml-base.so 5.3MB 5.3MB
libggml.so 477KB 476KB
合计 ~124MB ~45MB -64%

5. 后果分析

正面影响

负面影响

性能预期

指标 GPU (Vulkan) CPU (NEON+KleidiAI) 差异
Prefill (首 token) ~200ms ~500ms ~2.5x
Decode (生成) ~15 tokens/s ~6-8 tokens/s ~2x
稳定性 ❌ DeviceLost 崩溃 ✅ 无崩溃 决定性差异

6. 后续演进

6.1 如果未来需要 GPU LLM 推理

如果后续需要更大的模型(如 7B+),CPU 推理无法满足性能要求时,需采用以下方案之一:

方案 可行性 说明
独占 GPU 模式 LLM 推理时暂停美颜渲染 → 用户体验不可接受
Vulkan VkDevice 共享 让 ncnn 和 ggml 共享同一个 VkDevice 和 VkQueue
升级 Adreno 驱动 厂商驱动更新不可控
NPU/专用推理芯片 未来 等待高通 AI Engine 的标准化移动端 LLM SDK
远程推理 已实现 回退到 Cloudflare Gateway → Kimi API,已有完整链路

6.2 监控建议


7. 相关文件

文件 说明
agent-core/src/main/cpp/CMakeLists.txt C++ 编译配置,含 Vulkan 禁用注释
agent-core/src/main/jniLibs/arm64-v8a/ 编译产物目录
beauty-engine/src/main/cpp/ncnn_face_detector.cpp ncnn Vulkan 配置(net_.opt.use_vulkan_compute = true
docs/03-TECHNICAL-SPECS/LLM_ENGINE_MIGRATION_MNN_TO_LLAMACPP.md LLM 引擎迁移技术规范
ADRs/ADR-001-beauty-engine-architecture.md 美颜引擎架构(含 EGL 渲染线程设计)

8. 变更记录

日期 版本 变更内容 作者
2026-06-15 v1.0 初始版本,记录事件 1 + 事件 2 两次 GPU 争抢问题及决策 RD

维护者:RD Agent 最后更新:2026-06-15 状态:已接受