HQBSH.com的whois记录显示注册于2013年1月18日,至今已经持续运营了:0年0个月0天零0小时0分钟0秒

最新报价
 找回密码
 立即注册

QQ登录

只需一步,快速开始

查看: 23|回复: 0

[求助] 索尼 Xperia 10 本地大模型推理报错:一次 ggml 内存分配失败的完整排查

[复制链接]

270

主题

1

回帖

156

银子

超级版主

积分
5808
发表于 | 显示全部楼层 |阅读模式
[TITLE]
索尼 Xperia 10 本地大模型推理报错:一次 ggml 内存分配失败的完整排查
[/TITLE]

[CATEGORY]
tech
[/CATEGORY]

[KEYWORDS]
华强北,KimiClaw,科技数码,AI,热点
[/KEYWORDS]

[FID]
69
[/FID]

[CONTENT]# 索尼 Xperia 10 本地大模型推理报错:一次 ggml 内存分配失败的完整排查

在索尼 Xperia 10 上想把 7B 的 GGUF 模型跑起来,坑基本都集中在这一步。现象还挺统一:Termux 里 `llama-cli` 刚打出 `llama_model_load: loading model`,屏幕停顿几秒,进程就没了,终端只剩一个 `Killed`。没有 Python traceback,也没有 OOM 提示,模型文件看着完好无损。

动手查之前,有件事得先认下来——这既不是 llama.cpp 的 bug,也不是你模型下坏了。进程是被 Android 的 lowmemorykiller 收走的。

## 现象:进程消失,而不是报错退出

复现路径很固定。Xperia 10,8GB 版本,骁龙 6 系平台,Termux 里自编译的 llama.cpp,模型用 `Qwen2.5-7B-Instruct-Q4_K_M.gguf`(约 4.7GB),上下文保持默认的 4096。

真正的证据在系统侧,不在应用侧:

```bash
adb logcat -b events | grep -iE "lowmemorykiller|am_kill"
```

`adj=900`,翻译过来就是"可回收,直接 SIGKILL"。同一时间 `dumpsys meminfo` 里 Termux 的 PSS 已经顶到 5.6GB 附近。8GB 物理内存减掉系统常驻,留给单个前台应用的天花板差不多也就这高度。

顺带说一句,`Killed` 这个输出本身就是线索。真是 `malloc` 失败的话,glibc/bionic 会返回 NULL,llama.cpp 会打印 `failed to allocate buffer`,进程能体面退出。被杀了就不一样,shell 只能拿到退出码 137(128+9),日志里空空如也。这两者的区别,直接决定你该去翻应用日志还是系统日志。

## 先把三个假故障排掉,再谈内存

很多人一上手就调参数。其实该先排除三类"长得像内存问题"的故障,否则后面全是白忙。

**模型文件本身不完整。** GGUF 经常分片下载,Hugging Face 断流的时候,文件大小对得上,但尾部全是零。`sha256sum` 跟仓库里的哈希对一下,或者 `head -c 4 model.gguf` 看魔数是不是 `GGUF`,十秒钟的事。

**Termux 装成了 32 位。** 这个最阴。32 位进程单进程虚拟地址空间上限大约 3GB,用户态实际能用更少,4.7GB 的模型压根映射不进来,`mmap` 直接失败,或者加载到一半被杀。怎么判断:

```bash
uname -m          # 应为 aarch64,出现 armv7l / armv8l 就是 32 位
getprop ro.product.cpu.abi   # 应为 arm64-v8a
```

还有一点,Termux 主程序和它的插件必须同源,都从 F-Droid 或者都从 GitHub Releases 拿,混装会因为签名不同报 `INSTALL_FAILED_SHARED_USER_INCOMPATIBLE`。Google Play 上那个版本长期标着实验分支,功能有缺失,别拿它当排查基线。

**模型丢在 `/sdcard` 上。** Android 10 之后 `/sdcard` 走 FUSE,`mmap` 这种按页随机读的模式在 FUSE 上性能惨不忍睹。加载阶段会冒出大量小 IO,看着像卡死,其实是在龟速读页。把模型 `cp` 到 Termux 私有目录(`$HOME`,也就是 `/data/data/com.termux/files/home`,ext4)再跑,同一份文件加载时间经常差出好几倍:

```bash
cp /sdcard/Download/model.gguf ~/models/
```

## LMKD 到底是怎么挑人的

Android 的 `lmkd` 早不是"剩余内存低于某个值就杀进程"那套老逻辑了。按 AOSP 官方文档的说法,它靠两类信号触发:内核的 `vmpressure` 事件,或者 PSI(pressure stall information)内存压力监控。再结合 memory cgroup 对每个进程做资源限制,按 `oom_score_adj` 排序挑牺牲者。

拆开看是三层。

**第一层,PSI 判压力。** 内核统计"因为等内存而 stall 的时间占比",超阈值就通知 `lmkd`。这个指标比"还剩多少可用内存"灵敏得多,它反映的是真实卡顿程度,不是账面数字。

**第二层,`oom_score_adj` 定顺序。** 前台应用大概在 0 附近,可见服务 100~200,缓存进程 700~900,空进程 900~999。Termux 当前台 Activity 跑的时候 adj 很低,可一旦切后台、屏幕熄灭,或者被系统判成"无可见界面",adj 唰一下就滑到 900 区间。这时候它就是第一批被收割的。

**第三层,memcg 兜底。** Android 给应用的内存 cgroup 设了上限,触到上限不一定立刻杀,但会把该进程排除在 page cache 之外,压力进一步加剧。

Reddit 的 r/LocalLLaMA 上有份挺实在的实测反馈:8GB 机器上 1B Q4 稳,3B 在 KV cache 涨起来之后开始被杀,8B 只能说"实验性质能加载"。还有人直接给了量化结论——模型 + KV cache + 运行时内存的活跃占用超过 2~3GB,LMKD 和单进程配额就要动手了。

这跟我这边 Xperia 10 上 PSS 5.6GB 才被杀看着矛盾,其实是机型差异。索尼把影像增强、音频处理、显示补偿这些服务做得很重,系统侧预留更多,应用侧账面能涨得更高,可 PSI 早就红透了。

## 内存到底花在哪,把账算清楚

"4.7GB 的模型,8GB 内存怎么会不够"——因为 4.7GB 只是权重。

| 项目 | 7B Q4_K_M / ctx 4096 | 说明 |
|---|---|---|
| 模型权重 | ~4.7GB | GGUF 文件大小 |
| KV cache | ~224MB | 全量预分配,与 ctx 成正比 |
| 计算缓冲区 | 200~400MB | 与 batch size、序列长度相关 |
| 运行时与堆 | 150~300MB | Termux、libc、分词器、mmap 元数据 |
| 页缓存残留 | 数百 MB | mmap 加载后仍挂在 page cache |

加起来 5.4~5.9GB,正好对上 `dumpsys meminfo` 的读数。

KV cache 值得单独算一遍,这是最容易被忽略的大头。公式:

```
每 token 字节数 = 2 × 层数 × KV 头数 × head_dim × 精度字节
```

Qwen2.5-7B 用了 GQA,28 层、4 个 KV 头、head_dim 128、fp16 存储:

```
2 × 28 × 4 × 128 × 2 = 57344 B ≈ 56 KB/token
ctx 4096 → 224 MB
ctx 2048 → 112 MB
```

关键在于 llama.cpp 默认按 `-c` 指定的长度**一次性预分配**整个 KV cache,不是用多少占多少。所以把 `-c 4096` 改成 `-c 2048`,省下的不是"可能用到的那部分",是实打实的 112MB,而且是进程启动那一刻就省下来。同样的道理,`Qwen2.5-3B` 的 KV 每 token 约 36KB,`-c 4096` 也才 144MB。小模型在中端机上更容易活下来,原因就在这。

还有个常见误解是"有 zram 兜底"。Android 确实普遍开了 zram swap,但它是在内存里做压缩,不是把页写到磁盘。Q4 量化的权重是接近随机分布的高熵数据,压缩率极低,通常不到 1.2:1。指望它救 7B,方向就错了,反而白耗 CPU 和一块内存去做压缩字典。

## 原因一:单应用内存上限,不是物理内存不够

Android 的 `LMKD` 按 `oom_score_adj` 和 `PSI` 压力打分,不看你"还剩几个 GB"。Xperia 10 这种中端机,系统常驻偏重,索尼的影像与音频服务挂了一堆,留给应用的实际预算比标称内存紧张多了。

`mmap` 加载模型时页缓存会一路涨。一旦碰到 PSI 压力阈值,LMKD 优先干掉"可重启"的应用——Termux 正好在名单里。

有个事要澄清:`--no-mmap` 并不是"省掉一份内存"。两种方式最终都会让权重页进 page cache,差别在控制权。`mmap` 是惰性缺页,加载进度和内存增长搅在一起,你很难判断卡在哪一步;`read()` 是主动顺序读,加载曲线单调,出问题一眼看得出是读到第几块崩的。中端机上,可预测性比理论最优值钱。

再提一个反直觉的点:**别加 `--mlock`**。它把页锁死在物理内存里,阻止回收,等于主动把自己的 `oom_score_adj` 往枪口上推。桌面服务器上这是好东西,手机上就是自杀。

## 原因二:把 NNAPI 当 NPU 用

不少人的第一反应是加 `--n-gpu-layers 99` 或者开 NNAPI delegate,盼着加速。Xperia 10 用的是 6 系平台,Hexagon 的 AI 算力定位跟 8 系完全是两码事。NNAPI 上多数 transformer 算子没有对应实现,最后还是 fallback 回 CPU,白白多一层 graph 拆分开销。

这事背后还有个更根本的背景:**NNAPI 在 Android 15 已经被官方标记为弃用**。Android Developers 的迁移指南写得很直白,NNAPI 从 Android 8.1 引入,在 Android 15 弃用,官方建议性能敏感场景迁到替代方案,并预期未来大多数设备会回到 CPU 后端。llama.cpp 仓库里关于 NNAPI 后端的 issue 也一直卡在算子对齐、反量化和指针内存布局上,Android 17 上甚至认不出提交过去的 `MATMUL`。说白了,这条路目前是半废弃状态,你调它,调的是一段没人维护的代码。

实测结果:开 NNAPI 反而慢 30% 左右,还多占一份算子缓冲区,更容易被杀。

6 系平台上真正能用的加速路子就两条。一条是 Vulkan,把部分层 offload 到 Adreno,注意 Vulkan 库在只读的 `/system/lib64/libvulkan.so`,编译时要显式指定路径。另一条是 ARM 的 dotprod 指令集,Cortex-A78/A55 都支持,llama.cpp 会用它做 Q4_0 的 repacking,纯 CPU 下也能拿到不错的提升。native 编译时 `-DGGML_NATIVE=ON` 会自动探测,别手贱关掉。

## 原因三:线程数与 thermald 的连锁反应

默认线程数按 CPU 核心拉满。中端平台的小核参与矩阵乘法之后,温度蹭蹭往上走,`thermald` 开始限频,主频掉到 1.2GHz 以下。解码速度从 4 tok/s 掉到 1 tok/s 都不到,用户以为"卡死",其实它还在跑。

"线程越多越快"在大小核架构上是彻底错的。有人(@termuxSolutions)在 Galaxy S26(Exynos 2600)上用 LFM2.5 1.2B Q8_0 做过一组对照,数据挺有说服力:

| 配置 | prompt 处理 (t/s) | 生成 (t/s) |
|---|---|---|
| 全 10 核 CPU (`-t 10`) | 289 | 21.9 |
| 8 线程 CPU (`-t 8`) | 535 | 29.9 |
| 3 线程绑大核 + `-ngl 40` | 932 | 47.0 |
| Vulkan 全 offload (`-ngl 99 -t 2`) | — | 45~48 |

三个线程绑大核,生成速度是全十核的两倍多。原因不复杂:矩阵乘法是同步屏障式的,一轮计算得等所有线程到齐,小核跑得慢,大核就干等着。核越多,木桶效应越明显。

想知道自己机器上哪些是大核,直接看最大频率:

```bash
cat /sys/devices/system/cpu/cpu*/cpufreq/cpuinfo_max_freq
```

Xperia 10 这类 6 系平台通常是 2 个 A78 加 6 个 A55。把 `-t` 设成 2~4,再用 `taskset` 或者 llama.cpp 的 affinity 选项绑到高频核上,比拉满 8 线程稳得多,也凉快得多。

测温这么看:

```bash
adb shell cat /sys/class/thermal/thermal_zone*/temp
```

单位是 0.001℃,读到 65000 就是 65℃。中端机跑到 70℃ 往上,`thermald` 的限频基本就启动了,这时候再谈性能没有意义。

## 解决步骤

**1. 先降模型规格,别急着调参数。** 8GB 中端机跑 7B Q4 是硬边界。换 `Qwen2.5-3B-Instruct-Q4_K_M`(约 2GB)或者 7B 的 `IQ2_XXS`,先把流程跑通。IQ2 系列在 2bit 上做了重要性矩阵校准,7B 能压到 2.4GB 左右,质量比同尺寸的 Q2_K 明显好,代价是解码慢一点。先达成"能跑",再往上加规格。

**2. 固定上下文和批大小,压住峰值内存。** context 从 4096 降到 2048,batch 从 512 降到 128,KV cache 占用直接减半:

```bash
./llama-cli -m model.gguf -c 2048 -b 128 -t 4 --no-mmap
```

`-t 4` 绑在中核上,避开小核的高延迟和发热。`--no-mmap` 用显式 read 替代映射,加载过程可预测,代价是首次加载慢个 3~5 秒,换来的是内存曲线单调可控。

如果只做短问答、意图识别这类活儿,`-c 1024` 甚至 512 都够用。上下文长度和内存是线性关系,这是唯一一个"调小一定有用且没有副作用"的参数。

**3. 关掉不必要的常驻。** 推理前清一次后台,别指望系统自己让路:

```bash
adb shell am kill-all
adb shell cmd package suspend com.sonymobile.*
```

索尼的影像增强、音频处理、显示补偿在推理期间一点价值都没有,挂起后能腾出 300~500MB。`suspend` 对系统关键包会失败,报错忽略就行,不影响结果。顺便把屏幕常亮打开——息屏会压低 Termux 的进程优先级,adj 一涨就容易被顺手回收。

**4. 编译期关掉 OpenMP。** Termux 里 `-DGGML_OPENMP=ON` 会创建跟核心数相同的线程池,和 `-t` 参数打架,线程栈本身也吃内存(默认每线程 8MB,8 线程就是 64MB 虚拟空间):

```bash
cmake -B build -DGGML_OPENMP=OFF -DLLAMA_CURL=OFF -DCMAKE_BUILD_TYPE=Release
cmake --build build --config Release -j4
```

`-DLLAMA_CURL=OFF` 顺手去掉 libcurl 依赖,能省几 MB 常驻和一个网络栈。本地跑模型,根本用不到它。

**5. 用 bench 建基线,别靠体感判断。** `llama-bench -m model.gguf -p 64 -n 64 -t 4` 跑三遍,记下 pp/tg 两个数值。改任何参数后重测,只认数字。

想再进一层,扫一遍线程数找拐点:

```bash
llama-bench -m model.gguf -t 2,3,4,6,8 -p 256 -n 64
```

多数中端机在 3~4 线程处见顶,再往上加就是纯发热。把这条曲线存着,以后换模型、换系统版本都有参照。

**6. 想长期挂着,换个容器。** 如果你打算让模型常驻,比如给 KimiClaw 这类本地 agent 做离线兜底,`llama-cli` 的前台进程模型就不太合适了。用 `llama-server` 起一个 localhost 服务,再配合 `termux-wake-lock` 阻止系统休眠,比反复重启 CLI 稳得多:

```bash
termux-wake-lock
./llama-server -m model.gguf -c 2048 -t 4 --host 127.0.0.1 --port 8080
```

`--host 127.0.0.1` 千万别改成 `0.0.0.0`。公共 Wi-Fi 下,那等于把模型接口敞开给整层楼。

## 一条判断链,省掉反复试错

查到最后你会发现,这类问题的路径其实是收敛的。按顺序走,比乱调参数快得多:

- 输出 `Killed` 且退出码 137 → 系统层问题,看 `am_kill`,别翻应用日志
- 加载到 2GB 左右必崩,跟模型大小无关 → 先查 `uname -m` 是不是 32 位
- 加载正常但一提问就死 → KV cache + batch 的峰值,降 `-c` 和 `-b`
- 能跑但越来越慢 → thermald,看 `thermal_zone` 温度,降线程数
- 前台正常、切后台就没 → adj 升高被杀,加 wake-lock 或保持前台
- 换了 NNAPI 反而更慢 → 直接关掉,回 CPU + dotprod

在华强北的二手市场上,Xperia 10 系列因为日系小屏和 3.5mm 耳机孔一直有稳定需求。8GB 版本比 6GB 贵不了几个钱,但跑本地模型时,这 2GB 就是"能加载"和"加载即死"的分界线。买之前认清版本,比事后调参有用得多。

## 常见问题

### 1. 索尼 Xperia 10 本地大模型推理怎么部署/安装?

先确认运行环境,系统版本、运行时、依赖版本都对上,再按官方文档走一遍最小可用部署。上线前把日志、监控和回滚策略补齐,别等服务挂了才想起来。

### 2. 索尼 Xperia 10 本地大模型推理生产环境建议怎么配置?

关键是先明确目标场景,然后按"最小可用→逐步增强"的路子迭代配置和性能指标。一上来就堆配置,出了问题根本没法定位。

### 3. 什么是索尼 Xperia 10 本地大模型推理?适合用在什么场景?

通常指与本文主题相关的核心对象或方案。可以从三个点去理解:它解决什么问题、依赖哪些组件、怎么跟现有系统集成。

## 小结

Xperia 10 上的失败,几乎都不是"模型太大",而是把桌面端的调参习惯直接搬到了手机上。以为内存够、以为 NNAPI 能加速、以为线程越多越快。三个假设全不成立的时候,进程就被系统安安静静地收走了。

真正有效的动作其实就两条:把峰值内存压到 LMKD 阈值以下,把线程数压到散热能承受的范围。剩下的参数微调,收益远小于这两条。手机跑本地模型这件事,本质是在一个被系统严格看管的预算里做减法,不是做加法。

你手上那台机器,`-t` 最后试到几?评论区聊聊实测数据。
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册 QQ登录

本版积分规则

在线客服
马上联系
加好友78950405
微信联系8618938079527
微信联系
电话联系
联系电话18938079527
工作时间
11:00-22:00