端侧推理排查实录:为什么我的手机跑 LLM 比别人慢 80 倍

为什么你的手机跑 LLM 比别人慢 80 倍:一次端侧推理排查实录

标签:端侧推理 / llama.cpp / Android / 性能优化


一、先说结果

我在 Pixel 4(骁龙 855,2019 年)上跑 Qwen2.5-1.5B-Instruct Q4_K_M, 目标是做一个完全离线的 Android 对话应用。

结果是:

指标 实测
生成速度 0.5 tok/s
首 token 延迟 1.9 秒
模型加载 3-24 秒
峰值内存 1.3-2.0 GB
CPU 占用 402%(4 线程满载)

0.5 tok/s 是什么概念? 一个字要等两秒。能用,但谈不上愉快。

但真正让我在意的不是"慢",而是------

理论上不该这么慢。


二、算一笔账:慢 80 倍

先算理论极限。

内存带宽瓶颈(跑 LLM 的主要限制):

bash 复制代码
模型大小 1.06 GB
骁龙 855 内存带宽约 34 GB/s
每生成 1 个 token 需要读一遍权重
→ 理论极限 = 34 / 1.06 ≈ 32 tok/s

算力瓶颈:

ini 复制代码
每个 token 计算量 = 1.5B 参数 × 2 FLOPs = 3 GFLOPs
骁龙 855 CPU 峰值 ≈ 182 GFLOPS(4×A76 @2.84GHz,NEON)
→ 理论极限 = 182 / 3 ≈ 60 tok/s

两个理论值都在 30-60 tok/s 区间。实测 0.5 tok/s,差了 60-120 倍。

而且 CPU 已经 402% 满载了------它在拼命算,但算得极其低效。

这就是排查的起点。


三、排查过程

怀疑 1:线程数没生效?(排除)

第一反应是"是不是只用了 1 个核"。

用 top 采样:

bash 复制代码
adb shell top -b -n 1 -o PID,%CPU,%MEM,ARGS

输出:

perl 复制代码
PID    %CPU   %MEM   ARGS
17834  402%   24.3%  com.example.localai

402% ------ 4 个线程都在满载运行。线程没问题,排除。


怀疑 2:编译时没有启用 ARM 优化?(命中)

这是最有价值的发现。

llama.cpp 的核心计算在 ggml 里。我翻它的 CMake 配置:

cmake 复制代码
# ggml/src/ggml-cpu/CMakeLists.txt
if (GGML_NATIVE)
    # 探测本机 CPU 特性,自动加 -mcpu=native
    ...
else()
    if (GGML_CPU_ARM_ARCH)
        list(APPEND ARCH_FLAGS -march=${GGML_CPU_ARM_ARCH})   # ← 关键
    elseif(GGML_CPU_ALL_VARIANTS)
        ...
    # 两个都没设置 → 一个 -march 都不加
endif()

问题就在这里:

  • GGML_NATIVE 在交叉编译时是 OFF(不能探测目标设备)
  • GGML_CPU_ARM_ARCH 默认是空字符串
  • 结果:编译时完全不传 -march

编译器会用什么?ARMv8-A 的基础指令集 ------ 没有 dotprod、没有 fp16。

后果: 量化矩阵乘法(LLM 推理 99% 的算力所在)退化成慢速实现。

修复:

kotlin 复制代码
// app/build.gradle.kts
arguments += listOf(
    "-DUSE_LLAMA_CPP=ON",
    "-DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod+fp16"
)

效果:prefill 从 114 秒降到 45 秒,提速 2.5 倍。

有进步,但......

验证:优化真的生效了吗?

不能只看"编译参数写了"。我用 llvm-objdump 反汇编产物:

bash 复制代码
llvm-objdump -d libggml-cpu.so | findstr "sdot"

输出:

makefile 复制代码
18f558: 4e829420    sdot   v0.4s, v1.16b, v2.16b

sdot 指令确实存在 (这是 ARM dotprod 的核心指令)。优化生效了,排除。

但速度还是 0.5 tok/s。


怀疑 3:模型页被系统回收?(排除)

下一个怀疑:内存访问。

Android 内存压力大时会回收 mmap 的页,导致每次读权重都回落到 flash。 如果真是这样,速度会是:

bash 复制代码
flash 读取约 1.5 GB/s
1.06 GB / 1.5 GB/s ≈ 700 ms/token

这个数量级和实测的 2 秒/token 很接近!

于是改用强制驻留内存的加载模式:

cpp 复制代码
mparams.load_mode = LLAMA_LOAD_MODE_MLOCK;   // 读进物理内存并锁定

结果:0.5 tok/s,没有改善。排除。


四、我在过程中犯的三个错误

排查过程中我因为"凭记忆写代码",踩了三个坑。写出来给大家避雷。

错误 1:以为 llama_batch_get_one 会把位置固定为 0

我一开始认为:

"llama_batch_get_one() 把 batch 的 pos 固定为 0,所以每生成一个 token 都从头重算,这是慢的原因。"

于是花时间手写了 llama_batch_init() + 显式设置 pos。

结果:

  1. 查头文件发现 llama_batch_get_one 的 pos 是 nullptr ,意思是由 llama.cpp 自动分配位置,不是 0。
  2. 我手写的版本反而把 prefill 改卡住了(43 tokens 的 prefill 卡住不返回)。
  3. 回退到 llama_batch_get_one 后恢复正常。

教训:性能问题的第一嫌疑是编译选项和内存访问,不是"我猜的某个算法细节"。

错误 2:用了已被移除的 API

cpp 复制代码
llama_kv_cache_clear(g_ctx);   // 新版已移除

新版 llama.cpp 改成了:

cpp 复制代码
llama_memory_clear(llama_get_memory(g_ctx), true);   // 正确

错误 3:猜结构体字段名

cpp 复制代码
mparams.use_mmap  = false;   // 错误,字段已不存在
mparams.use_mlock = true;    // 错误

新版合并成了一个枚举:

cpp 复制代码
mparams.load_mode = LLAMA_LOAD_MODE_MLOCK;   // 正确

这三条的共同教训:llama.cpp 的 API 演进非常快,任何函数名、字段名都必须先查 include/llama.h,凭记忆写必然踩坑。


五、结论

排除了三个怀疑后,剩下的是:

ini 复制代码
每个 token 计算量 3 GFLOPs
实测 1 token = 2 秒
→ 实际 CPU 算力利用率 ≈ 0.8%

0.8% 的峰值利用率。 而且不是内存带宽瓶颈(实测 ~500 MB/s,设备有 34 GB/s)。

也就是说:计算效率本身有问题,但已经超出"配置错误"的范畴------ 可能是 Android 内核的 CPU 调度、OpenMP 线程同步开销、或者 llama.cpp 在这个平台上的实现差异。

这不是一两天能解决的,而且------它不该挡住产品的交付。


六、我的处理方式

  1. 接受现状,把它写清楚:在项目 README 里如实标注 "Pixel 4: 0.5 tok/s"
  2. 标注已知限制的原因:老设备 + 大模型的客观组合
  3. 邀请社区补充数据:请骁龙 8 Gen 2/3 用户提交实测数据
  4. 把性能优化作为 v2 计划,而不是阻塞 v1 发布

七、可迁移的经验

经验 说明
端侧推理慢,先查编译选项 -march / NEON / dotprod 是最常见原因,不是算法
CPU 满载 ≠ 效率正常 402% 占用可能只是在跑最慢的实现
先算理论极限 知道差多少倍,才知道该怀疑什么
用反汇编验证优化 编译参数写了 ≠ 指令真的生成了
查头文件,不要凭记忆 llama.cpp API 变化极快
给作品设置"止损点" 性能优化不该无限期阻塞交付

附录:完整配置

项 值
设备 Google Pixel 4(骁龙 855,6GB RAM,Android 13)
模型 Qwen2.5-1.5B-Instruct-Q4_K_M.gguf(1065 MB)
llama.cpp commit 8a1a9b5(2026-10-09)
NDK 30.0.16248370
CMake 4.1.2
编译参数 -DGGML_CPU_ARM_ARCH=armv8.2-a+dotprod+fp16,-DUSE_LLAMA_CPP=ON
运行参数 n_threads=4, n_ctx=2048, load_mode=MLOCK

源码

完整实现(Kotlin + JNI + llama.cpp + Jetpack Compose)已开源:

github.com/Pingredsai/...


如果你在更新的设备上跑同一个模型,欢迎把数据发给我------这正是这个项目最需要的。

相关推荐
荆棘鸟智能2 小时前
野生动物细粒度识别模型怎么做?从相机陷阱数据清洗到边缘部署
人工智能·目标检测
Smoothcloud润云2 小时前
GPU服务器租用实例DNS与HTTPS下载排障实战
大数据·运维·服务器·人工智能·https·gpu算力
沐风___2 小时前
Computer Use 怎么用:从 Codex 界面验证到 LCU 接入
人工智能
十铭忘2 小时前
四大坐标系转换1——针孔相机模型和凸透镜成像
人工智能
七牛云行业应用2 小时前
GitHub 94k Star 工具 Agent Reach:给 AI Agent 装上互联网能力,零 API 费用接入 13 个平台
人工智能·github
wshzd2 小时前
LLM之Agent(108)|DeepSeek-Harness(十六)上下文压缩 Compaction 与 Checkpoint 持久化
人工智能
武子康2 小时前
两台 A6000,LingBot 应该先验哪条路径?
人工智能·后端·agent
老纪的技术唠嗑局2 小时前
# 来啦!PowerContext v1.2.0,接入 jev!
人工智能
AAASilverwolf3 小时前
Gemini Nano Banana 2.1 正式可用:视觉设计、文字渲染和主体一致性到底提升在哪?
人工智能·api