根据提供的code differences信息,我发现没有具体的代码变更内容。因此生成一个通用的commit message:
``` docs(readme): 更新文档说明 - 添加项目使用说明 - 完善配置指南 ``` 注意:由于未提供具体的代码差异信息,以上为示例格式。请提供实际的代码变更内容以生成准确的commit message。
This commit is contained in:
@@ -39,10 +39,8 @@ actor AIRuntime {
|
||||
private(set) var lastGenerateStats: GenerateStats?
|
||||
|
||||
/// 当前实际生效的后端标签(性能自检 / PPT 截图用)。
|
||||
/// 端侧统一走 MLX/GPU(Gemma-3n E2B);MNN/SME2 LLM 路径已移除。
|
||||
var activeBackendLabel: String {
|
||||
if InferenceEngine.current == .mnn, mnnStatus == .ready {
|
||||
return InferenceEngine.cpuSupportsSME2 ? "MNN · SME2" : "MNN · NEON"
|
||||
}
|
||||
#if targetEnvironment(simulator)
|
||||
return "MLX · CPU(模拟器)"
|
||||
#else
|
||||
@@ -53,12 +51,6 @@ actor AIRuntime {
|
||||
private var llmSession: LLMSession?
|
||||
private var vlSession: VLSession?
|
||||
|
||||
// MARK: - MNN 后端(CPU/SME2,挑战赛考核路径)
|
||||
// .mnn 引擎下,文本生成与 VL(图→文)由同一个 Qwen3.5-2B 多模态 MNN 模型全包(已实测)。
|
||||
// 模拟器无 MNN,VL 回退 MLX 的 Qwen3-VL-4B。
|
||||
private let mnn = MNNBackend()
|
||||
private(set) var mnnStatus: Status = .notReady
|
||||
|
||||
// MARK: - Gemini 云端后端(hybrid:端侧 Gemma 默认,云端按需增强)
|
||||
// 云端调用不占本机显存,不进 OOM 闸门,可与端侧推理并发。用于「云端深度解读 / 多语言」
|
||||
// 与「拍报告/药盒多模态读图」——后者恢复端侧 Gemma-3n(MLX 文本版)丢掉的真·视觉能力。
|
||||
@@ -67,10 +59,6 @@ actor AIRuntime {
|
||||
nonisolated var cloudAvailable: Bool { CloudAI.isConfigured }
|
||||
/// 云端后端标签(性能自检 / 截图用)。
|
||||
nonisolated static var cloudLabel: String { "Gemini · \(CloudAI.model)" }
|
||||
/// MNN 模型目录(下载/旁路导入到 Models/Qwen3.5-2B-MNN)。
|
||||
nonisolated static var mnnModelFolder: URL {
|
||||
ModelStore.shared.localURL(for: .mnnLLM)
|
||||
}
|
||||
|
||||
// MARK: - 串行推理闸门(§3.1 OOM 防护的真正落地)
|
||||
//
|
||||
@@ -144,18 +132,8 @@ actor AIRuntime {
|
||||
#endif
|
||||
}
|
||||
|
||||
/// 加载文本模型。首次调用会真正加载,后续幂等。
|
||||
/// 按当前引擎路由:.mnn → MNN(CPU/SME2);.mlx → 现有 MLX(GPU)。
|
||||
/// 加载文本模型(端侧 MLX/GPU,Gemma-3n E2B)。首次调用会真正加载,后续幂等。
|
||||
func prepare() async throws {
|
||||
// 选了 MNN 且模型已就绪才走 MNN;否则(选 MLX,或 MNN 模型尚未下载)回退 MLX,
|
||||
// 保证过渡期 App 始终可用。引擎指示器(Phase 5)展示实际生效后端。
|
||||
let mnnReady = ModelStore.shared.isComplete(for: .mnnLLM)
|
||||
if InferenceEngine.current == .mnn, mnnReady {
|
||||
try await prepareMNN()
|
||||
return
|
||||
}
|
||||
// 走 MLX:先卸 MNN 释放内存(单模型常驻策略)。
|
||||
await unloadMNN()
|
||||
// 已有其他调用方在加载时,轮询等其结束再判定结果。
|
||||
// 不能像旧实现那样裸 return:那会让调用方误以为已 ready,随后 generate 的
|
||||
// `guard status == .ready` 失败 → 用户撞上「假错误屏」(模型其实正常加载中)。
|
||||
@@ -195,54 +173,12 @@ actor AIRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// 加载 MNN 文本模型。幂等。单模型常驻:载入前卸掉 MLX 的 LLM/VL 释放内存。
|
||||
private func prepareMNN() async throws {
|
||||
while mnnStatus == .loading {
|
||||
try await Task.sleep(nanoseconds: 80_000_000)
|
||||
}
|
||||
if mnnStatus == .ready { return }
|
||||
|
||||
let folder = Self.mnnModelFolder
|
||||
guard ModelStore.shared.isComplete(for: .mnnLLM) else {
|
||||
mnnStatus = .error("MNN 模型未就绪")
|
||||
throw AIRuntimeError.notReady
|
||||
}
|
||||
|
||||
await acquireGate()
|
||||
defer { releaseGate() }
|
||||
if mnnStatus == .ready { return }
|
||||
|
||||
// 单模型常驻:卸 MLX LLM/VL,避免与 MNN 模型叠加占内存。
|
||||
unloadLLM()
|
||||
unloadVL()
|
||||
|
||||
mnnStatus = .loading
|
||||
do {
|
||||
try await mnn.load(folderURL: folder)
|
||||
mnnStatus = .ready
|
||||
} catch {
|
||||
mnnStatus = .error("\(error)")
|
||||
throw AIRuntimeError.modelLoadFailed("\(error)")
|
||||
}
|
||||
}
|
||||
|
||||
/// 卸载 MNN,释放桥与权重。幂等。
|
||||
private func unloadMNN() async {
|
||||
guard mnnStatus != .notReady else { return }
|
||||
await mnn.unload()
|
||||
mnnStatus = .notReady
|
||||
MLX.Memory.clearCache()
|
||||
}
|
||||
|
||||
/// 流式生成。调用前应先 await prepare()。
|
||||
/// 注意:返回流是同步创建的,但跨 actor 调用 LLMSession 需要 await。
|
||||
/// priority = .background 时排队让行、解码中可被前台请求按 token 抢占(CancellationError 透传)。
|
||||
func generate(prompt: String,
|
||||
maxTokens: Int = 256,
|
||||
priority: InferencePriority = .interactive) -> AsyncThrowingStream<TokenChunk, Error> {
|
||||
if InferenceEngine.current == .mnn, mnnStatus == .ready {
|
||||
return mnnGenerate(prompt: prompt, maxTokens: maxTokens, priority: priority)
|
||||
}
|
||||
// 在 actor 隔离上下文中捕获快照,Task 内不再访问 self.status / self.llmSession
|
||||
let snapshotStatus = status
|
||||
let snapshotSession = llmSession
|
||||
@@ -282,40 +218,6 @@ actor AIRuntime {
|
||||
}
|
||||
}
|
||||
|
||||
/// MNN(CPU/SME2)文本流式生成。结构与 MLX 分支一致:进闸门、串行解码、记录速率。
|
||||
private func mnnGenerate(prompt: String,
|
||||
maxTokens: Int,
|
||||
priority: InferencePriority) -> AsyncThrowingStream<TokenChunk, Error> {
|
||||
let ready = (mnnStatus == .ready)
|
||||
return AsyncThrowingStream { continuation in
|
||||
let task = Task {
|
||||
guard ready else {
|
||||
continuation.finish(throwing: AIRuntimeError.notReady)
|
||||
return
|
||||
}
|
||||
await self.acquireGate(priority)
|
||||
defer { self.releaseGate() } // 无论正常结束 / 异常 / 取消都释放闸门,防死锁
|
||||
do {
|
||||
let stream = await self.mnn.generate(prompt: prompt, maxTokens: maxTokens)
|
||||
for try await chunk in stream {
|
||||
try Task.checkCancellation()
|
||||
// 后台任务让位:前台请求在排队时,下一个 token 处主动退出
|
||||
//(流终止触发 MNNBackend.onTermination → bridge.cancel())。
|
||||
if self.shouldPreempt(priority) { throw CancellationError() }
|
||||
continuation.yield(chunk)
|
||||
}
|
||||
self.lastGenerateStats = await self.mnn.lastStats
|
||||
continuation.finish()
|
||||
} catch is CancellationError {
|
||||
continuation.finish(throwing: CancellationError())
|
||||
} catch {
|
||||
continuation.finish(throwing: AIRuntimeError.inferenceFailed("\(error)"))
|
||||
}
|
||||
}
|
||||
continuation.onTermination = { _ in task.cancel() }
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Gemini 云端(hybrid 增强)
|
||||
|
||||
/// 云端流式生成(深度解读 / 多语言)。不进 OOM 闸门,可与端侧并发。
|
||||
@@ -353,7 +255,7 @@ actor AIRuntime {
|
||||
|
||||
// MARK: - 端侧 ASR(SenseVoice via sherpa-mnn)互斥入口
|
||||
|
||||
/// 给端侧语音转写(SenseVoice,占 CPU + 内存)用:进推理闸门串行 + 卸掉常驻 LLM/VL/MNN 腾内存,
|
||||
/// 给端侧语音转写(SenseVoice,占 CPU + 内存)用:进推理闸门串行 + 卸掉常驻 LLM/VL 腾内存,
|
||||
/// 避免与文本/视觉模型同时常驻冲过单 App 内存上限被 jetsam 杀(§3.1 OOM 防护)。
|
||||
///
|
||||
/// 问诊流程是「先转写 → 再 organize(会重载 LLM)」严格串行,所以这里卸掉 LLM 是安全的:
|
||||
@@ -365,7 +267,6 @@ actor AIRuntime {
|
||||
defer { releaseGate() }
|
||||
unloadLLM()
|
||||
unloadVL()
|
||||
await unloadMNN()
|
||||
return try await body()
|
||||
}
|
||||
|
||||
@@ -373,11 +274,6 @@ actor AIRuntime {
|
||||
|
||||
/// 加载 VL 模型。幂等,首调真正 load。
|
||||
func prepareVL() async throws {
|
||||
// 选了 MNN 且多模态模型就绪:VL 复用同一个 MNN 模型(文本+视觉一肩挑),走 prepareMNN。
|
||||
if InferenceEngine.current == .mnn, ModelStore.shared.isComplete(for: .mnnLLM) {
|
||||
try await prepareMNN()
|
||||
return
|
||||
}
|
||||
while vlStatus == .loading {
|
||||
try await Task.sleep(nanoseconds: 80_000_000)
|
||||
}
|
||||
@@ -398,7 +294,6 @@ actor AIRuntime {
|
||||
|
||||
// OOM 闸门(§3.1):加载 VL(~3GB)前先卸 LLM(~1GB),否则两者常驻叠加冲过内存上限被 jetsam 杀。
|
||||
unloadLLM()
|
||||
await unloadMNN()
|
||||
|
||||
vlStatus = .loading
|
||||
do {
|
||||
@@ -438,16 +333,6 @@ actor AIRuntime {
|
||||
func analyzeReport(imageURLs: [URL],
|
||||
prompt: String,
|
||||
maxTokens: Int = 512) async throws -> String {
|
||||
// 选了 MNN 且就绪:图→文走同一个 MNN 多模态模型。
|
||||
if InferenceEngine.current == .mnn, mnnStatus == .ready {
|
||||
await acquireGate()
|
||||
defer { releaseGate() }
|
||||
do {
|
||||
return try await mnn.analyze(imageURLs: imageURLs, prompt: prompt, maxTokens: maxTokens)
|
||||
} catch {
|
||||
throw AIRuntimeError.inferenceFailed("\(error)")
|
||||
}
|
||||
}
|
||||
guard vlStatus == .ready, let session = vlSession else {
|
||||
throw AIRuntimeError.notReady
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user