Flutter 使用 llama_cpp_dart 加载 GGUF 模型失败,如何排查?
现有日志只能确认原生模型加载失败,无法确定唯一原因。load_llama_model_from_file 返回 null 表示加载失败;要定位原因,需要检查实际模型文件,并取得失败前的原生日志。代码中应先检查文件复制逻辑:目标文件一旦存在就会跳过复制,旧文件或不完整文件可能因此被反复加载。
现有日志说明了什么?
初始化过程已经输出 CPU 特性、modelPath 和 libraryPath,随后出现:
LlamaException: Could not load model at /data/user/0/com.example.frontend/files/model.gguf
这表明执行过程至少到达了原生相关代码,但不能据此认定模型有效、所有原生依赖兼容,或模型所需内存充足。
日志中的 OnBackInvokedCallback 警告与 Android 返回操作配置有关,现有信息没有表明它导致了模型加载失败。界面绘制和掉帧信息也无法解释加载函数为何返回 null。
先检查实际读取的模型文件
原代码只根据文件是否存在决定是否复制:
final fileExists = await File(filePath).exists();
if (!fileExists) {
// Copy the bundled model.
}
这里没有验证文件大小或内容。如果之前复制中断,或者后来替换了 assets 中的模型,应用仍可能读取支持目录里的旧文件。
可以在创建 LlamaLoad 前加入以下检查。它只读取前四个字节,避免为了检查文件头而把整个模型再次读入内存:
import 'dart:io';
import 'package:flutter/foundation.dart';
Future<void> inspectModelFile(String filePath) async {
final file = File(filePath);
if (!await file.exists()) {
throw StateError('Model file does not exist: $filePath');
}
final size = await file.length();
debugPrint('Model path: $filePath');
debugPrint('Model size: $size bytes');
if (size < 4) {
throw StateError('Model file is too small: $size bytes');
}
final handle = await file.open(mode: FileMode.read);
try {
final header = await handle.read(4);
debugPrint('Model header: $header');
const magic = <int>[0x47, 0x47, 0x55, 0x46];
if (header.length != magic.length ||
!List.generate(magic.length, (i) => header[i] == magic[i])
.every((matches) => matches)) {
throw StateError('Model file does not have a GGUF header');
}
} finally {
await handle.close();
}
}
调用位置:
await inspectModelFile(filePath);
final cmd = LlamaLoad(
path: filePath,
modelParams: ModelParams()..nGpuLayers = 0,
contextParams: ContextParams()
..nCtx = 512
..nThreads = 4,
samplingParams: SamplerParams()..temp = 0.8,
verbose: true,
);
GGUF 文件头正确,只能通过最基本的格式检查,不能证明整个模型完整或受当前运行库支持。还应核对:
- 目标文件大小是否与打包前的模型一致。
- 如果有可信的 SHA-256,目标文件哈希是否匹配。
- 文件是否真的是模型,而不是下载错误页面或 Git LFS 指针文件。
如果目标文件是可从 assets 恢复的模型副本,可以在停止使用模型后删除该副本,再重新复制。正式实现应在复制完成并校验后,才将文件作为可加载模型使用,避免仅凭 exists() 判断。
原代码已经使用 await file.writeAsBytes(..., flush: true)。没有证据表明必须改成同步写入,也不能仅凭返回 null 就认定文件句柄仍被占用。
获取失败前的原生日志
当前异常经过多层包装,缺少原生加载器报告的具体错误。保持 verbose: true,同时在终端运行:
adb logcat -v threadtime
然后重新触发加载,查看异常之前是否出现以下信息:
| 日志内容 | 优先检查方向 |
|---|---|
| 无法打开文件、读取失败、意外 EOF | 路径、文件长度、复制过程、文件完整性 |
| 不支持的 GGUF 版本、模型架构或张量类型 | 模型与实际 llama.cpp 构建的兼容性 |
| 分配失败、映射失败、内存不足 | 设备可用内存、模型大小和加载时的内存占用 |
| 缺少符号、共享库或后端加载失败 | 原生库、依赖库和构建配置 |
现有日志还不能证明上述任何一种原因。如果 Logcat 中仍只有 Dart 异常,需要检查当前安装版本的原生日志接入方式;不能假定 verbose: true 已经把全部原生错误转发到了 Flutter 控制台。
核对模型和原生库是否匹配
^0.2.2 是依赖版本约束,实际解析版本应查看 pubspec.lock。同时确认 APK 中实际打包的 libllama.so 来自哪里,以及它对应的 llama.cpp 构建版本。
需要一起核对模型架构、GGUF 格式版本、量化类型,以及原生加载器对它们的支持情况。文件扩展名相同,不代表模型能够被任意 llama.cpp 构建加载。
如果 libllama.so 是手动替换的,还要确认它与 Dart 绑定使用的原生接口匹配。仅换入一个较新的 .so 不能保证兼容。现有材料不足以判断 llama_cpp_dart 对应构建是否支持这个模型,也不足以指定一个必定有效的升级版本。
可以通过对照实验缩小范围:
- 在同一设备、同一套原生库下,加载一个已确认适配该构建的小模型。
- 如果有条件,用对应 llama.cpp 构建的工具加载当前模型。
- 比较结果和原生日志,区分文件问题、模型支持问题及设备资源问题。
内存和现有参数怎么查?
nGpuLayers = 0 表示当前配置没有请求 GPU 层卸载,不能保证 CPU 加载一定成功。模型权重和运行时仍需要内存。
原代码通过 rootBundle.load() 将模型资产读入内存,再写入文件。大模型在复制阶段也会占用内存,因此文件大小不等于整个加载过程的内存需求。若日志指向内存不足,可以先用更小的受支持模型验证加载链路,再评估模型规模及复制方式。
目前没有证据表明 nThreads = 4 或 temp = 0.8 导致模型文件加载失败;降低 nCtx 也不能解决文件损坏或模型格式不受支持的问题。五分钟超时只限制等待时间,延长它无法修复已经返回 null 的加载操作。
建议先检查文件大小、文件头和哈希,再获取原生错误。若仍无法定位,需要补充准确的模型名称、来源、量化类型、文件大小、设备内存、实际依赖版本及原生库构建信息,才能继续判断。
备注:内容仅供参考。
