feat(AI): 添加MLX内存管理和AI模型互斥卸载机制

为防止应用因内存溢出被系统终止,在项目中添加了MLX框架依赖,
并在应用启动时配置GPU缓存限制,设置256MB缓存上限以避免内存过度使用。

同时实现了LLM和VL模型的互斥卸载机制,确保大模型不会同时常驻内存,
通过在加载一个模型前先卸载另一个模型来控制内存使用,防止jetsam OOM。

chore(project): 配置代码签名授权文件

refactor(localization): 调整本地化字符串并清理冗余条目

修正了提醒任务和建议相关的本地化文本,调整了多个UI字符串,
清理了过时和重复的本地化条目,更新了AI识别相关的新字符串资源。
```
This commit is contained in:
link2026
2026-05-31 23:22:50 +08:00
parent db7cc1bba7
commit d72a1fec17
5 changed files with 161 additions and 34 deletions

View File

@@ -408,6 +408,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "康康/康康.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = F2C8C774FG; DEVELOPMENT_TEAM = F2C8C774FG;
@@ -459,6 +460,7 @@
buildSettings = { buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor;
CODE_SIGN_ENTITLEMENTS = "康康/康康.entitlements";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 2; CURRENT_PROJECT_VERSION = 2;
DEVELOPMENT_TEAM = F2C8C774FG; DEVELOPMENT_TEAM = F2C8C774FG;

View File

@@ -1,4 +1,5 @@
import Foundation import Foundation
import MLX
enum AIRuntimeError: Error, LocalizedError { enum AIRuntimeError: Error, LocalizedError {
case notReady case notReady
@@ -33,6 +34,16 @@ actor AIRuntime {
private init() {} 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 { func prepare() async throws {
switch status { switch status {
@@ -52,6 +63,10 @@ actor AIRuntime {
throw AIRuntimeError.notReady throw AIRuntimeError.notReady
} }
// OOM (§3.1):LLM(~1GB) VL(~3GB), App jetsam
// LLM VL, ModelContainer + MLX
unloadVL()
status = .loading status = .loading
do { do {
let session = try await LLMSession.load( let session = try await LLMSession.load(
@@ -120,6 +135,10 @@ actor AIRuntime {
throw AIRuntimeError.notReady throw AIRuntimeError.notReady
} }
// OOM (§3.1): VL(~3GB) LLM(~1GB), jetsam
// App 退
unloadLLM()
vlStatus = .loading vlStatus = .loading
do { do {
let session = try await VLSession.load( 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 ) /// JSON ( VLPrompts.reportExtraction )
/// + 退(§3.2) /// + 退(§3.2)
/// AIRuntime actor, LLM.generate() , OOM /// AIRuntime actor, LLM.generate() , OOM

View File

@@ -5,6 +5,11 @@ import SwiftData
struct KangkangApp: App { struct KangkangApp: App {
@State private var lang = LanguageManager.shared @State private var lang = LanguageManager.shared
init() {
// MLX , entitlement + LLM/VL jetsam OOM
AIRuntime.configureMLXMemory()
}
var sharedModelContainer: ModelContainer = { var sharedModelContainer: ModelContainer = {
let schema = Schema([ let schema = Schema([
Indicator.self, Indicator.self,

View File

@@ -31,9 +31,6 @@
}, },
"·" : { "·" : {
},
"· · ·" : {
}, },
"· %lld" : { "· %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 个提醒任务" : { "%lld 个提醒任务" : {
"localizations" : { "localizations" : {
"en" : { "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 个月" : { "%lld 个月" : {
"localizations" : { "localizations" : {
"en" : { "en" : {
@@ -886,9 +883,6 @@
}, },
"+%lld" : { "+%lld" : {
},
"< 3.40" : {
}, },
"⚠️ 通知权限已关闭,去「设置 → 康康 → 通知」打开" : { "⚠️ 通知权限已关闭,去「设置 → 康康 → 通知」打开" : {
"localizations" : { "localizations" : {
@@ -977,9 +971,6 @@
} }
} }
} }
},
"3.84" : {
}, },
"100% 本地推理 · 模型仅需下载一次" : { "100% 本地推理 · 模型仅需下载一次" : {
"localizations" : { "localizations" : {
@@ -1002,6 +993,9 @@
} }
} }
} }
},
"100%% 本地推理 · 已用 %llds" : {
}, },
"2026 / 05 / 25 · 协和医院体检中心" : { "2026 / 05 / 25 · 协和医院体检中心" : {
"localizations" : { "localizations" : {
@@ -1092,6 +1086,7 @@
} }
}, },
"AI 已识别到 1 项指标" : { "AI 已识别到 1 项指标" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -1376,9 +1371,6 @@
}, },
"kg" : { "kg" : {
},
"LDL-C" : {
}, },
"lo" : { "lo" : {
@@ -1485,6 +1477,9 @@
} }
} }
} }
},
"VL 模型未就绪,手动补充" : {
}, },
"VL 输出无法解析:%@" : { "VL 输出无法解析:%@" : {
"localizations" : { "localizations" : {
@@ -1991,6 +1986,9 @@
} }
} }
} }
},
"仅核对用 · 不保存照片" : {
}, },
"今天" : { "今天" : {
"localizations" : { "localizations" : {
@@ -2235,6 +2233,7 @@
} }
}, },
"低密度脂蛋白 3.84 mmol/L ↑" : { "低密度脂蛋白 3.84 mmol/L ↑" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -3027,6 +3026,7 @@
} }
}, },
"全部保存(%lld" : { "全部保存(%lld" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -3249,6 +3249,7 @@
} }
}, },
"再拍一项" : { "再拍一项" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -3803,6 +3804,9 @@
} }
} }
} }
},
"去设置" : {
}, },
"参考" : { "参考" : {
"localizations" : { "localizations" : {
@@ -4243,6 +4247,9 @@
} }
} }
} }
},
"图片编码失败,手动补充或重拍" : {
}, },
"在「+ 新建 → 指标记录 → %@」记录一次" : { "在「+ 新建 → 指标记录 → %@」记录一次" : {
"localizations" : { "localizations" : {
@@ -4889,6 +4896,7 @@
} }
}, },
"对准异常的那一行就好 · 不用拍整张" : { "对准异常的那一行就好 · 不用拍整张" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -5153,6 +5161,9 @@
} }
} }
} }
},
"已取消识别,手动补充或重拍" : {
}, },
"已处理 %.1fs · 比云端快 4.2×" : { "已处理 %.1fs · 比云端快 4.2×" : {
"localizations" : { "localizations" : {
@@ -5353,6 +5364,7 @@
} }
}, },
"已裁剪" : { "已裁剪" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -5838,6 +5850,9 @@
} }
} }
} }
},
"异常项快拍需要相机。去「设置 → 康康 → 相机」打开后再回来。" : {
}, },
"强度" : { "强度" : {
"localizations" : { "localizations" : {
@@ -6058,6 +6073,9 @@
} }
} }
} }
},
"快超时了,>%llds 会自动转手动录入" : {
}, },
"性别" : { "性别" : {
"localizations" : { "localizations" : {
@@ -6104,6 +6122,7 @@
} }
}, },
"总胆固醇 TC 5.42 mmol/L" : { "总胆固醇 TC 5.42 mmol/L" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -6410,6 +6429,9 @@
} }
} }
} }
},
"把异常项放进框里 · 对准一两行" : {
}, },
"抑郁/焦虑" : { "抑郁/焦虑" : {
"localizations" : { "localizations" : {
@@ -6545,6 +6567,9 @@
} }
} }
} }
},
"拍到的局部" : {
}, },
"拍报告的小贴士" : { "拍报告的小贴士" : {
"localizations" : { "localizations" : {
@@ -6567,6 +6592,9 @@
} }
} }
} }
},
"拍摄异常项" : {
}, },
"拍摄报告" : { "拍摄报告" : {
"localizations" : { "localizations" : {
@@ -6811,6 +6839,7 @@
} }
}, },
"指标名 · 可编辑" : { "指标名 · 可编辑" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -8402,6 +8431,7 @@
} }
}, },
"本次已记录 %lld 项" : { "本次已记录 %lld 项" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -8565,6 +8595,7 @@
} }
}, },
"核对后一次保存" : { "核对后一次保存" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -8585,6 +8616,9 @@
} }
} }
} }
},
"核对异常项" : {
}, },
"核对识别结果" : { "核对识别结果" : {
"localizations" : { "localizations" : {
@@ -9028,6 +9062,12 @@
} }
} }
} }
},
"没有识别到指标,点「加一项」手动补充,或返回重拍" : {
},
"没读出指标,手动补充或重拍" : {
}, },
"测试 PROMPT" : { "测试 PROMPT" : {
"localizations" : { "localizations" : {
@@ -9272,6 +9312,7 @@
} }
}, },
"甘油三酯 TG 1.78 mmol/L" : { "甘油三酯 TG 1.78 mmol/L" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -9603,6 +9644,9 @@
}, },
"症状详情" : { "症状详情" : {
},
"相机权限未开启" : {
}, },
"程度" : { "程度" : {
@@ -9630,6 +9674,7 @@
} }
}, },
"空腹血糖 GLU" : { "空腹血糖 GLU" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -10032,6 +10077,7 @@
} }
}, },
"继续拍下一项" : { "继续拍下一项" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -10338,6 +10384,9 @@
} }
} }
} }
},
"范围" : {
}, },
"范围 %@ %@" : { "范围 %@ %@" : {
"localizations" : { "localizations" : {
@@ -10725,6 +10774,9 @@
} }
} }
} }
},
"识别到的指标 (%lld)" : {
}, },
"识别失败:%@" : { "识别失败:%@" : {
"localizations" : { "localizations" : {
@@ -10747,6 +10799,9 @@
} }
} }
} }
},
"识别框内指标" : {
}, },
"识别没有读出指标,请手动补充" : { "识别没有读出指标,请手动补充" : {
"localizations" : { "localizations" : {
@@ -10771,6 +10826,7 @@
} }
}, },
"识别用时 0.4s · 本地" : { "识别用时 0.4s · 本地" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -10857,6 +10913,9 @@
} }
} }
} }
},
"识别超时(>%llds),手动补充或重拍" : {
}, },
"该测%@了" : { "该测%@了" : {
"localizations" : { "localizations" : {
@@ -11013,6 +11072,7 @@
} }
}, },
"超过参考上限 0.44属轻度偏高。建议关注饮食结构减少动物脂肪摄入3 个月内复查。若家族有心血管病史,可与医生沟通是否需要药物干预。" : { "超过参考上限 0.44属轻度偏高。建议关注饮食结构减少动物脂肪摄入3 个月内复查。若家族有心血管病史,可与医生沟通是否需要药物干预。" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -11035,6 +11095,7 @@
} }
}, },
"超过参考上限 0.44,属轻度偏高。点击展开详细解读 " : { "超过参考上限 0.44,属轻度偏高。点击展开详细解读 " : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -11233,6 +11294,7 @@
} }
}, },
"载脂蛋白 A1 1.42 g/L" : { "载脂蛋白 A1 1.42 g/L" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -11277,6 +11339,7 @@
} }
}, },
"载脂蛋白 B 1.04 g/L" : { "载脂蛋白 B 1.04 g/L" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {
@@ -11719,6 +11782,9 @@
} }
} }
} }
},
"重拍" : {
}, },
"重新生成" : { "重新生成" : {
"localizations" : { "localizations" : {
@@ -12073,6 +12139,7 @@
} }
}, },
"高密度脂蛋白 1.21 mmol/L" : { "高密度脂蛋白 1.21 mmol/L" : {
"extractionState" : "stale",
"localizations" : { "localizations" : {
"en" : { "en" : {
"stringUnit" : { "stringUnit" : {

View 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>