```
feat(AI): 添加MLX内存管理和AI模型互斥卸载机制 为防止应用因内存溢出被系统终止,在项目中添加了MLX框架依赖, 并在应用启动时配置GPU缓存限制,设置256MB缓存上限以避免内存过度使用。 同时实现了LLM和VL模型的互斥卸载机制,确保大模型不会同时常驻内存, 通过在加载一个模型前先卸载另一个模型来控制内存使用,防止jetsam OOM。 chore(project): 配置代码签名授权文件 refactor(localization): 调整本地化字符串并清理冗余条目 修正了提醒任务和建议相关的本地化文本,调整了多个UI字符串, 清理了过时和重复的本地化条目,更新了AI识别相关的新字符串资源。 ```
This commit is contained in:
@@ -408,6 +408,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "康康/康康.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = F2C8C774FG;
|
||||
@@ -459,6 +460,7 @@
|
||||
buildSettings = {
|
||||
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
|
||||
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
|
||||
CODE_SIGN_ENTITLEMENTS = "康康/康康.entitlements";
|
||||
CODE_SIGN_STYLE = Automatic;
|
||||
CURRENT_PROJECT_VERSION = 2;
|
||||
DEVELOPMENT_TEAM = F2C8C774FG;
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import Foundation
|
||||
import MLX
|
||||
|
||||
enum AIRuntimeError: Error, LocalizedError {
|
||||
case notReady
|
||||
@@ -33,6 +34,16 @@ actor AIRuntime {
|
||||
|
||||
private init() {}
|
||||
|
||||
/// App 启动时调用一次:给 MLX 的 GPU 缓冲池设上限,避免 reuse cache 在大模型常驻之上
|
||||
/// 继续膨胀、把峰值推过单 App 内存上限。仅真机生效(模拟器走 CPU,且部分 Metal 路径会 abort)。
|
||||
/// 与 increased-memory-limit entitlement + LLM/VL 互斥卸载配合,三管齐下防 jetsam OOM。
|
||||
nonisolated static func configureMLXMemory() {
|
||||
#if !targetEnvironment(simulator)
|
||||
// 256MB cache 上限:够复用、不至于在 3GB 模型之上再囤几百 MB 空闲缓冲。
|
||||
MLX.GPU.set(cacheLimit: 256 * 1024 * 1024)
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 加载模型。首次调用会真正加载,后续幂等。
|
||||
func prepare() async throws {
|
||||
switch status {
|
||||
@@ -52,6 +63,10 @@ actor AIRuntime {
|
||||
throw AIRuntimeError.notReady
|
||||
}
|
||||
|
||||
// OOM 闸门(§3.1):LLM(~1GB)与 VL(~3GB)不可同时常驻,叠加会冲过单 App 内存上限被 jetsam 杀。
|
||||
// 加载 LLM 前先卸 VL,释放其 ModelContainer + MLX 显存缓存。
|
||||
unloadVL()
|
||||
|
||||
status = .loading
|
||||
do {
|
||||
let session = try await LLMSession.load(
|
||||
@@ -120,6 +135,10 @@ actor AIRuntime {
|
||||
throw AIRuntimeError.notReady
|
||||
}
|
||||
|
||||
// OOM 闸门(§3.1):加载 VL(~3GB)前先卸 LLM(~1GB),否则两者常驻叠加冲过内存上限被 jetsam 杀
|
||||
// —— 这正是「异常项快拍识别时 App 自动退出」的主因。
|
||||
unloadLLM()
|
||||
|
||||
vlStatus = .loading
|
||||
do {
|
||||
let session = try await VLSession.load(
|
||||
@@ -133,6 +152,26 @@ actor AIRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - 卸载(OOM 闸门)
|
||||
|
||||
/// 卸载 LLM,释放 ModelContainer 引用并清 MLX 显存缓存。幂等。
|
||||
/// 注:若此刻有 generate() 的流仍在跑,它持有 session 快照,真正释放要等流结束;
|
||||
/// 但快拍/归档场景下没有并发文本流,卸载即时生效。
|
||||
private func unloadLLM() {
|
||||
guard llmSession != nil else { return }
|
||||
llmSession = nil
|
||||
status = .notReady
|
||||
MLX.GPU.clearCache()
|
||||
}
|
||||
|
||||
/// 卸载 VL,释放 ModelContainer 引用并清 MLX 显存缓存。幂等。
|
||||
private func unloadVL() {
|
||||
guard vlSession != nil else { return }
|
||||
vlSession = nil
|
||||
vlStatus = .notReady
|
||||
MLX.GPU.clearCache()
|
||||
}
|
||||
|
||||
/// 图像 → JSON 字符串(由 VLPrompts.reportExtraction 引导)。
|
||||
/// 调用方负责解析 + 失败回退(§3.2)。
|
||||
/// AIRuntime 是 actor,本调用与 LLM.generate() 自然串行,不会 OOM。
|
||||
|
||||
@@ -5,6 +5,11 @@ import SwiftData
|
||||
struct KangkangApp: App {
|
||||
@State private var lang = LanguageManager.shared
|
||||
|
||||
init() {
|
||||
// 启动即给 MLX 显存缓存设上限,配合 entitlement + LLM/VL 互斥卸载防 jetsam OOM。
|
||||
AIRuntime.configureMLXMemory()
|
||||
}
|
||||
|
||||
var sharedModelContainer: ModelContainer = {
|
||||
let schema = Schema([
|
||||
Indicator.self,
|
||||
|
||||
@@ -31,9 +31,6 @@
|
||||
},
|
||||
"·" : {
|
||||
|
||||
},
|
||||
"· · ·" : {
|
||||
|
||||
},
|
||||
"· %lld" : {
|
||||
|
||||
@@ -395,6 +392,28 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"%lld 个建议" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld suggestions"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld 件の提案"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "제안 %lld 개"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%lld 个提醒任务" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -439,28 +458,6 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"%lld 个建议" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld suggestions"
|
||||
}
|
||||
},
|
||||
"ja" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "%lld 件の提案"
|
||||
}
|
||||
},
|
||||
"ko" : {
|
||||
"stringUnit" : {
|
||||
"state" : "translated",
|
||||
"value" : "제안 %lld 개"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"%lld 个月" : {
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
@@ -886,9 +883,6 @@
|
||||
},
|
||||
"+%lld" : {
|
||||
|
||||
},
|
||||
"< 3.40" : {
|
||||
|
||||
},
|
||||
"⚠️ 通知权限已关闭,去「设置 → 康康 → 通知」打开" : {
|
||||
"localizations" : {
|
||||
@@ -977,9 +971,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"3.84" : {
|
||||
|
||||
},
|
||||
"100% 本地推理 · 模型仅需下载一次" : {
|
||||
"localizations" : {
|
||||
@@ -1002,6 +993,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"100%% 本地推理 · 已用 %llds" : {
|
||||
|
||||
},
|
||||
"2026 / 05 / 25 · 协和医院体检中心" : {
|
||||
"localizations" : {
|
||||
@@ -1092,6 +1086,7 @@
|
||||
}
|
||||
},
|
||||
"AI 已识别到 1 项指标" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -1376,9 +1371,6 @@
|
||||
},
|
||||
"kg" : {
|
||||
|
||||
},
|
||||
"LDL-C" : {
|
||||
|
||||
},
|
||||
"lo" : {
|
||||
|
||||
@@ -1485,6 +1477,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"VL 模型未就绪,手动补充" : {
|
||||
|
||||
},
|
||||
"VL 输出无法解析:%@" : {
|
||||
"localizations" : {
|
||||
@@ -1991,6 +1986,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"仅核对用 · 不保存照片" : {
|
||||
|
||||
},
|
||||
"今天" : {
|
||||
"localizations" : {
|
||||
@@ -2235,6 +2233,7 @@
|
||||
}
|
||||
},
|
||||
"低密度脂蛋白 3.84 mmol/L ↑" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -3027,6 +3026,7 @@
|
||||
}
|
||||
},
|
||||
"全部保存(%lld)" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -3249,6 +3249,7 @@
|
||||
}
|
||||
},
|
||||
"再拍一项" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -3803,6 +3804,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"去设置" : {
|
||||
|
||||
},
|
||||
"参考" : {
|
||||
"localizations" : {
|
||||
@@ -4243,6 +4247,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"图片编码失败,手动补充或重拍" : {
|
||||
|
||||
},
|
||||
"在「+ 新建 → 指标记录 → %@」记录一次" : {
|
||||
"localizations" : {
|
||||
@@ -4889,6 +4896,7 @@
|
||||
}
|
||||
},
|
||||
"对准异常的那一行就好 · 不用拍整张" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -5153,6 +5161,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"已取消识别,手动补充或重拍" : {
|
||||
|
||||
},
|
||||
"已处理 %.1fs · 比云端快 4.2×" : {
|
||||
"localizations" : {
|
||||
@@ -5353,6 +5364,7 @@
|
||||
}
|
||||
},
|
||||
"已裁剪" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -5838,6 +5850,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"异常项快拍需要相机。去「设置 → 康康 → 相机」打开后再回来。" : {
|
||||
|
||||
},
|
||||
"强度" : {
|
||||
"localizations" : {
|
||||
@@ -6058,6 +6073,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"快超时了,>%llds 会自动转手动录入" : {
|
||||
|
||||
},
|
||||
"性别" : {
|
||||
"localizations" : {
|
||||
@@ -6104,6 +6122,7 @@
|
||||
}
|
||||
},
|
||||
"总胆固醇 TC 5.42 mmol/L" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -6410,6 +6429,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"把异常项放进框里 · 对准一两行" : {
|
||||
|
||||
},
|
||||
"抑郁/焦虑" : {
|
||||
"localizations" : {
|
||||
@@ -6545,6 +6567,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"拍到的局部" : {
|
||||
|
||||
},
|
||||
"拍报告的小贴士" : {
|
||||
"localizations" : {
|
||||
@@ -6567,6 +6592,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"拍摄异常项" : {
|
||||
|
||||
},
|
||||
"拍摄报告" : {
|
||||
"localizations" : {
|
||||
@@ -6811,6 +6839,7 @@
|
||||
}
|
||||
},
|
||||
"指标名 · 可编辑" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -8402,6 +8431,7 @@
|
||||
}
|
||||
},
|
||||
"本次已记录 %lld 项" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -8565,6 +8595,7 @@
|
||||
}
|
||||
},
|
||||
"核对后一次保存" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -8585,6 +8616,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"核对异常项" : {
|
||||
|
||||
},
|
||||
"核对识别结果" : {
|
||||
"localizations" : {
|
||||
@@ -9028,6 +9062,12 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"没有识别到指标,点「加一项」手动补充,或返回重拍" : {
|
||||
|
||||
},
|
||||
"没读出指标,手动补充或重拍" : {
|
||||
|
||||
},
|
||||
"测试 PROMPT" : {
|
||||
"localizations" : {
|
||||
@@ -9272,6 +9312,7 @@
|
||||
}
|
||||
},
|
||||
"甘油三酯 TG 1.78 mmol/L" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -9603,6 +9644,9 @@
|
||||
},
|
||||
"症状详情" : {
|
||||
|
||||
},
|
||||
"相机权限未开启" : {
|
||||
|
||||
},
|
||||
"程度" : {
|
||||
|
||||
@@ -9630,6 +9674,7 @@
|
||||
}
|
||||
},
|
||||
"空腹血糖 GLU" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -10032,6 +10077,7 @@
|
||||
}
|
||||
},
|
||||
"继续拍下一项" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -10338,6 +10384,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"范围" : {
|
||||
|
||||
},
|
||||
"范围 %@ %@" : {
|
||||
"localizations" : {
|
||||
@@ -10725,6 +10774,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"识别到的指标 (%lld)" : {
|
||||
|
||||
},
|
||||
"识别失败:%@" : {
|
||||
"localizations" : {
|
||||
@@ -10747,6 +10799,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"识别框内指标" : {
|
||||
|
||||
},
|
||||
"识别没有读出指标,请手动补充" : {
|
||||
"localizations" : {
|
||||
@@ -10771,6 +10826,7 @@
|
||||
}
|
||||
},
|
||||
"识别用时 0.4s · 本地" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -10857,6 +10913,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"识别超时(>%llds),手动补充或重拍" : {
|
||||
|
||||
},
|
||||
"该测%@了" : {
|
||||
"localizations" : {
|
||||
@@ -11013,6 +11072,7 @@
|
||||
}
|
||||
},
|
||||
"超过参考上限 0.44,属轻度偏高。建议关注饮食结构(减少动物脂肪摄入),3 个月内复查。若家族有心血管病史,可与医生沟通是否需要药物干预。" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -11035,6 +11095,7 @@
|
||||
}
|
||||
},
|
||||
"超过参考上限 0.44,属轻度偏高。点击展开详细解读 ›" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -11233,6 +11294,7 @@
|
||||
}
|
||||
},
|
||||
"载脂蛋白 A1 1.42 g/L" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -11277,6 +11339,7 @@
|
||||
}
|
||||
},
|
||||
"载脂蛋白 B 1.04 g/L" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
@@ -11719,6 +11782,9 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"重拍" : {
|
||||
|
||||
},
|
||||
"重新生成" : {
|
||||
"localizations" : {
|
||||
@@ -12073,6 +12139,7 @@
|
||||
}
|
||||
},
|
||||
"高密度脂蛋白 1.21 mmol/L" : {
|
||||
"extractionState" : "stale",
|
||||
"localizations" : {
|
||||
"en" : {
|
||||
"stringUnit" : {
|
||||
|
||||
14
康康/康康.entitlements
Normal file
14
康康/康康.entitlements
Normal file
@@ -0,0 +1,14 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<!--
|
||||
抬高单 App 内存上限。8GB 设备(如 iPhone 15 Pro Max)默认 jetsam 上限约 ~3GB,
|
||||
VL 模型(Qwen2.5-VL-3B 4bit,常驻 + Metal 激活缓冲)会冲过该线被系统直接杀进程
|
||||
(表现为「拍照识别时 App 自动退出」)。此 entitlement 把上限抬到设备物理内存可承受的更高档位。
|
||||
仅 iOS 生效;macOS / 模拟器忽略。配合 AIRuntime 的 LLM/VL 互斥卸载使用。
|
||||
-->
|
||||
<key>com.apple.developer.kernel.increased-memory-limit</key>
|
||||
<true/>
|
||||
</dict>
|
||||
</plist>
|
||||
Reference in New Issue
Block a user