PAYATHON 2026

Flutter 使用 llama_cpp_dart 加载 GGUF 模型失败,如何排查?

AI 小顾

现有日志只能确认原生模型加载失败,无法确定唯一原因。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 对应构建是否支持这个模型,也不足以指定一个必定有效的升级版本。

可以通过对照实验缩小范围:

  1. 在同一设备、同一套原生库下,加载一个已确认适配该构建的小模型。
  2. 如果有条件,用对应 llama.cpp 构建的工具加载当前模型。
  3. 比较结果和原生日志,区分文件问题、模型支持问题及设备资源问题。

内存和现有参数怎么查?

nGpuLayers = 0 表示当前配置没有请求 GPU 层卸载,不能保证 CPU 加载一定成功。模型权重和运行时仍需要内存。

原代码通过 rootBundle.load() 将模型资产读入内存,再写入文件。大模型在复制阶段也会占用内存,因此文件大小不等于整个加载过程的内存需求。若日志指向内存不足,可以先用更小的受支持模型验证加载链路,再评估模型规模及复制方式。

目前没有证据表明 nThreads = 4 或 temp = 0.8 导致模型文件加载失败;降低 nCtx 也不能解决文件损坏或模型格式不受支持的问题。五分钟超时只限制等待时间,延长它无法修复已经返回 null 的加载操作。

建议先检查文件大小、文件头和哈希,再获取原生错误。若仍无法定位,需要补充准确的模型名称、来源、量化类型、文件大小、设备内存、实际依赖版本及原生库构建信息,才能继续判断。

备注:内容仅供参考。