写点什么

基于 Flutter 实现跨平台离线大模型对话应用

作者:轻口味
  • 2023-04-19
    北京
  • 本文字数:3782 字

    阅读完需:约 12 分钟

基于Flutter实现跨平台离线大模型对话应用

1、背景介绍

ChatGPT 的爆火让证明了大模型的可行,让各大公司趋之若鹜,疯狂拥抱。公司基于行业数据库训练的大模型在进行 4bit 量化后压缩为 4GB,尝试利用端的 CPU 能力离线部署运行在 Android、iOS、Mac、Windows、Linux。

考虑到跨平台的开发效率我们使用了 Google 推出的 Flutter。

2、项目实现

项目的功能很简单,UI 上一个最简单的 IM 聊天页面,左侧显示模型生成答案,右侧显示输入问题,底部是输入框和发送按钮。输入内容点击发送时,将文本内容传给模型,模型生成答案后流式返回结果展示。

这里聊天 UI 基于开源项目flyerhq/flutter_chat_ui 二次开发,UI 效果如下:


2.1 Flutter 调用 C/C++代码

加载模型使用 C/C++代码,Flutter 提供了dart:ffi 实现本地代码的调用。「FFI」 代表外部功能接口类似功能的其他术语包括「本地接口」「语言绑定」

int loadLibrary() {    var libraryPath;    if (Platform.isLinux) {      libraryPath = path.join(modelLibDir, 'lib$modelLibName.so');      modelLib = DynamicLibrary.open(libraryPath);    }    if (Platform.isAndroid) {      libraryPath = 'lib$modelLibName.so';      modelLib = DynamicLibrary.open(libraryPath);    }    if (Platform.isMacOS) {      modelLib = DynamicLibrary.process();    }    if (Platform.isWindows) {      libraryPath = path.join(modelLibDir, 'Debug', '$modelLibName.dll');      modelLib = DynamicLibrary.open(libraryPath);    }    if (Platform.isIOS) {      modelLib = DynamicLibrary.process();    }    funcModelInit = modelLib.lookupFunction<VoidPtrFuncCharPtr, VoidPtrFuncCharPtr>('xxx_init');    funcModelGenerate = modelLib.lookupFunction<VoidFuncVoidPtrCharPtrCallbackPtr, VoidFuncVoidPtrCharPtrCallbackPtrDart>('xxx_generate');    return 0;  }
复制代码

不同的平台调用本地代码实现略有不同,但是 Flutter 已经封装的足够通用了。

「本地代码编译」

我们将 C/C++源代码添加到 ios 文件夹,或者添加到一个单独目录再软链到ios/Classes下,因为 CocoaPods 不允许源码处于比 podspec 文件更高的目录层级,但是 Gradle 允许你指向 ios 文件夹。

FFI 库只能与 C 符号绑定,因此在 C++ 中,这些符号添加 extern C 标记。还应该添加属性来表明符号是需要被 Dart 引用的,以防止链接器在优化链接时会丢弃符号。

放置好代码后,在 Android 下面创建 CMakeLists.txt 目录,配置编译源文件和 target,然后在 build.gradle 中配置:

externalNativeBuild {        // Encapsulates your CMake build configurations.        cmake {            // Provides a relative path to your CMake build script.            path "CMakeLists.txt"        }    }
复制代码

path 指向 CmakeLists.txt 路径,这样在编译 Android 项目是会自动将 C/C++代码编译成动态库。

对于 iOS,放置到ios/Classes下后编译时会自动编译这部分代码。对于 Mac,和 iOS 类似。

对于 Windows,直接在 windows 目录下创建 CMakeLists.txt 文件,可以在 CMakeLists.txt 中直接添加编译信息:

cmake_minimum_required(VERSION 3.10)
# 项目名称set(PROJECT_NAME "libNativeAdd")project(${PROJECT_NAME} LANGUAGES CXX)
# 源文件add_library(${PROJECT_NAME} SHARED    "./native_add.cpp")
# 动态库的输出目录set(CMAKE_LIBRARY_OUTPUT_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/$<$<CONFIG:DEBUG>:Debug>$<$<CONFIG:RELEASE>:Release>")# 安装动态库的目标目录set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}")# 安装动态库,到执行目录install(FILES "${CMAKE_LIBRARY_OUTPUT_DIRECTORY}/${PROJECT_NAME}.dll" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" COMPONENT Runtime)
复制代码

也可以添加子目录方式添加对应模块:

add_subdirectory("../libs/native_add" native_add)
复制代码

2.2 Flutter 文件路径问题

由于模型比较大,有 4GB,如果直接打包到应用程序包中,替换和更新比较费劲,所以让应用程序读取固定路径下一个文件加载。在 Mac、Linux 下,直接定位当前应用路径即可:Directory.current.path,对于 Android,直接定位应用目录返回是/,我们需要使用对应路径,我们使用 Google 官方维护的插件 「path_provider」

「path_provider」提供了 8 个方法获取不同的文件路径:

  • 「getTemporaryDirectory」 :临时目录,适用于下载的缓存文件,此目录随时可以清除,此目录为应用程序私有目录,其他应用程序无法访问此目录。Android 上对应getCacheDir。iOS 上对应NSCachesDirectory

  • 「getApplicationSupportDirectory」:应用程序可以在其中放置应用程序支持文件的目录的路径。在 iOS 上,对应NSApplicationSupportDirectory ,如果此目录不存在,则会自动创建。在 Android 上,对应getFilesDir

  • 「getLibraryDirectory」:应用程序可以在其中存储持久性文件,备份文件以及对用户不可见的文件的目录路径,例如 storage.sqlite.db。在 Android 上,此函数抛出[UnsupportedError]异常,没有等效项路径存在。

  • 「getApplicationDocumentsDirectory」:应用程序可能在其中放置用户生成的数据或应用程序无法重新创建的数据的目录路径。在 iOS 上,对应NSDocumentDirectory API。 如果数据不是用户生成的,使用[getApplicationSupportDirectory]。在 Android 上,对应getDataDirectory API。 如果要让用户看到数据,改用[getExternalStorageDirectory]。

  • 「getExternalStorageDirectory」:应用程序可以访问顶级存储的目录的路径。由于此功能仅在 Android 上可用,因此应在发出此函数调用之前确定当前操作系统。在 iOS 上,此功能会引发[UnsupportedError]异常,因为无法在应用程序的沙箱外部访问。在 Android 上,对应getExternalFilesDir(null)

  • 「getExternalCacheDirectories」:存储特定于应用程序的外部缓存数据的目录的路径。 这些路径通常位于外部存储(如单独的分区或 SD 卡)上。 这里返回的是一个列表。该方法仅在 Android 上可用,在 iOS 上,此功能会抛出 UnsupportedError,因为这是不可能的在应用程序的沙箱外部访问。在 Android 上,对应Context.getExternalCacheDirs()或 API Level 低于 19 的Context.getExternalCacheDir()

  • 「getExternalStorageDirectories」:可以存储应用程序特定数据的目录的路径。 这些路径通常位于外部存储(如单独的分区或 SD 卡)上。此功能仅在 Android 上可用,在 iOS 上,此功能会抛出 UnsupportedError,因为这是不可能的在应用程序的沙箱外部访问。在 Android 上,对应Context.getExternalFilesDirs(String type)或 API Level 低于 19 的Context.getExternalFilesDir(String type)

  • 「getDownloadsDirectory」:存储下载文件的目录的路径,这通常仅与台式机操作系统有关。在 Android 和 iOS 上,此函数将引发[UnsupportedError]异常。

Andorid 上为了省去动态权限的申请,我们直接放在getExternalCacheDirectories下:

Future<String> _getTemporaryDirectory() async {    List<Directory>? dirs = await getExternalCacheDirectories();    return dirs!.first.path;  }
复制代码

iOS 无法读取沙盒外的数据,所以我们只能将模型打包到应用程序,放置在assets下,在 pubspec.yaml 下进行配置。然后启动应用时将 assets 下的模型文件释放到临时目录下:

  Future<String> readAndWriteModel() async {    String fileName = "xxx.bin";    String dir = (await getTemporaryDirectory()).path;    String filePath = "$dir/$fileName";    print("readAndWriteModel path = $filePath");    File file = await new File(filePath);    if(!await file.exists()){      var bytes = await rootBundle.load("assets/xxx.bin");      ByteBuffer buffer =  bytes.buffer;      file.writeAsBytes(buffer.asUint8List(bytes.offsetInBytes,        bytes.lengthInBytes));    }
    return filePath;  }
复制代码

3. 运行效果展示

Android 运行效果:

https://www.bilibili.com/video/BV1xs4y127i2/

设备:vivo iQoo neo 7 3.2GHz 天玑 9000+ 八核 12+8GB 内存 独立显示芯片​模型:约 4GB

GPU 加速:未开启​效果:最开始出字慢,后面整体还可以,手机会发烫

性能数据(内从从最开始 2.5G 增长到 6.5GB):



其他手机都没有 vivo 这块效果好,华为 Mate Xs 处理器 HUAWEIKirin9905G、运行内存 8.0GB 性能数据:



4、遇到问题

  1. iOS 编译运行到 iphone14 时一直报签名失败,xcode 版本问题,切换手机后正常;

  2. iphone12 ProMax 运行时报 out of memory 错误,应为 iphone 不支持 swap 内存,并且最大内存 3G,无法全量加载模型,现在正在继续对模型裁剪。

5、参考资料

6、总结

本文介绍了离线大模型对话应用的跨平台实现,包括 Flutter 调用本地代码,Flutter 跨平台路径问题,展示了离线大模型效果及性能指标。

发布于: 2023-04-19阅读数: 20
用户头像

轻口味

关注

🏆2021年InfoQ写作平台-签约作者 🏆 2017-10-17 加入

Android、音视频、AI相关领域从业者。 欢迎加我微信wodekouwei拉您进InfoQ音视频沟通群 邮箱:qingkouwei@gmail.com

评论

发布
暂无评论
基于Flutter实现跨平台离线大模型对话应用_flutter_轻口味_InfoQ写作社区