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运行效果:
设备:vivo iQoo neo 7 3.2GHz 天玑9000+ 八核 12+8GB 内存 独立显示芯片 模型:约4GB
GPU加速:未开启 效果:最开始出字慢,后面整体还可以,手机会发烫
性能数据(内从从最开始2.5G增长到6.5GB):
其他手机都没有vivo这块效果好,华为Mate Xs 处理器HUAWEIKirin9905G、运行内存8.0GB性能数据:
4、遇到问题
- iOS编译运行到iphone14时一直报签名失败,xcode版本问题,切换手机后正常;
- iphone12 ProMax运行时报out of memory错误,应为iphone不支持swap内存,并且最大内存3G,无法全量加载模型,现在正在继续对模型裁剪。
5、参考资料
6、总结
本文介绍了离线大模型对话应用的跨平台实现,包括Flutter调用本地代码,Flutter跨平台路径问题,展示了离线大模型效果及性能指标。